在线时间 1630 小时 最后登录 2024-1-29 注册时间 2017-5-16 听众数 82 收听数 1 能力 120 分 体力 563399 点 威望 12 点 阅读权限 255 积分 174243 相册 1 日志 0 记录 0 帖子 5313 主题 5273 精华 3 分享 0 好友 163
TA的每日心情 开心 2021-8-11 17:59
签到天数: 17 天
[LV.4]偶尔看看III
网络挑战赛参赛者
网络挑战赛参赛者
自我介绍 本人女,毕业于内蒙古科技大学,担任文职专业,毕业专业英语。
群组 : 2018美赛大象算法课程
群组 : 2018美赛护航培训课程
群组 : 2019年 数学中国站长建
群组 : 2019年数据分析师课程
群组 : 2018年大象老师国赛优
太狠了,疫情期间面试,一个问题砍了我5000!
2 ?! l3 g/ Y" |6 z# \ 疫情期间找工作确实有点难度,想拿到满意的薪资,确实要点实力啊!
. Z; q: D! ?- f8 d8 A6 n 6 Q9 ~; T* b0 H6 e( R
面试官:Spring中的@Value用过么,介绍一下
M, [3 L7 @0 v
4 g. @4 `, I1 ^- @8 V3 O 我:@Value可以标注在字段上面,可以将外部配置文件中的数据,比如可以将数据库的一些配置信息放在配置文件中,然后通过@Value的方式将其注入到bean的一些字段中
0 j: j' V+ D% ]1 I% G, o
& v! Q. G2 f: c1 Q* V 面试官:那就是说@Value的数据来源于配置文件了?
8 e7 u6 x0 g, t) y" \ 4 i# i/ f1 j, [; L$ A
我:嗯,我们项目最常用更多就是通过@Value来引用Properties文件中的配置
- K/ o3 d5 i2 |) m; O
9 W* y/ [/ M3 _0 X8 x 面试官:@Value数据来源还有其他方式么?6 m2 |+ X, G6 Y0 m) S2 G
8 M# q1 Y( X: |$ H- Z; } 我:此时我异常开心,刚好问的我都研究过,我说:当然有,可以将配置信息放在db或者其他存储介质中,容器启动的时候,可以将这些信息加载到Environment中,@Value中应用的值最终是通过Environment来解析的,所以只需要扩展一下Environment就可以实现了。
" o2 Y* t+ G' r2 ^5 j: L& M0 b0 e' D7 x + u3 I$ x) I" A# I5 H `
面试官:不错嘛,看来你对spring研究的还是可以,是不是喜欢研究spring源码?
/ n& I1 j& @3 j/ H) D `. n4 d5 H
! Z) s% ^% ~$ V# x0 Z 我:笑着说,嗯,平时有空的时候确实喜欢捣鼓捣鼓源码,感觉自己对spring了解的还可以,不能算精通,也算是半精通吧
q k3 X) O: Y # D0 \4 E& U8 P" G" T. }1 l+ T
面试官:看着我笑了笑,那@Value的注入的值可以动态刷新么?9 a- Z4 |) m: m) x+ b9 v
- C/ h0 o8 M6 h: |8 c+ v
我:应该可以吧,我记得springboot中有个@RefreshScope注解就可以实现你说的这个功能. R" ?+ Z! m/ g# U; K8 i
/ t1 L( E' ^4 P- J6 A6 g
面试官:那你可以说一下@RefreshScope是如何实现的么,可以大概介绍一下?
0 r) D$ [+ t0 w( X
5 {0 x) n! f3 y3 x 我:嗯。。。这个之前看过一点,不过没有看懂! H# Q9 x0 J7 Z( ~! A* g. U
/ h m1 J2 x/ X* x
面试官:没关系,你可以回去了再研究一下;你期望工资多少?
2 ~" p6 L8 \9 ]% { k8 z ' f$ i/ Y! t$ o9 t% z
我:3万吧
6 t' H" u5 A2 d( h2 o . O: J+ g. b! N3 _; ^, x& U$ ~
面试官:今天的面试还算是可以的,不过如果@RefreshScope能回答上来就更好了,这块是个加分项,不过也确实有点难度,2.5万如何?/ ~5 m# n$ V: j% c; z$ U
/ G" Q! ^. p O, T
我:(心中默默想了想:2.5万,就是一个问题没有回答好,砍了5000,有点狠啊,我要回去再研究研究,3万肯定是没问题的),我说:最低2.9万
( N. O: R; Z# {: y$ I
$ r4 v6 Y4 y! { 面试官:那谢谢你,今天面试就到这里,出门右拐,不送!$ y$ e4 ?( ^9 A! o
2 r' F2 Q+ j/ p: j
我有个好习惯,每次面试回去之后,都会进行复盘,把没有搞定的问题一定要想办法搞定,这样才不虚。$ x) y# e7 k( Q: t+ i: f
3 g- [' c! @" `0 A% N
这次面试问题如下
# w0 d* p! P6 k \1 w
6 ]! L7 ~0 t/ i! N6 }1 U9 A @Value的用法
& u8 X* U) w9 j, P$ H8 J
, z: w6 S' D) Z! Y! H3 j# L" l! q @Value数据来源$ z% E, v) f/ }; x. Q: w# p7 ]
1 |* B2 R b7 |5 X# t( G$ u; L @Value动态刷新的问题9 F: R$ Q% {4 A# `) t
2 Y3 ]) n8 ?/ x: J. s4 S
下面我们一个个来整理一下,将这几个问题搞定,助大家在疫情期间面试能够过关斩将,拿高薪。
1 Y. @1 m& o& d- _3 R
6 D# K v2 S8 h @Value的用法5 N' [5 _; O0 v+ S- ^2 W1 n& S
7 p6 u; x' f& x! J2 l
系统中需要连接db,连接db有很多配置信息。- H- x6 r# P& v" k
: i; u% D& r1 Q% k, R C
系统中需要发送邮件,发送邮件需要配置邮件服务器的信息。
. r( H- h" v& H- g! b, H1 t
* r0 z. S" H' d, J | 还有其他的一些配置信息。. Z3 L, a/ v8 g
3 v; D0 h$ y3 @1 i8 W: y 我们可以将这些配置信息统一放在一个配置文件中,上线的时候由运维统一修改。
: s8 v# E; f7 u% _9 b) a. T
' @7 @2 x# ]& c0 [' \. U 那么系统中如何使用这些配置信息呢,spring中提供了@Value注解来解决这个问题。
+ E7 i! K. T# N0 J2 m: B
( P7 g9 [2 J. h1 C 通常我们会将配置信息以key=value的形式存储在properties配置文件中。% K3 Q. [& [- z) {7 c. ^9 p5 B
* k& B8 D# L( T 通过@Value("${配置文件中的key}")来引用指定的key对应的value。' A5 O3 J1 J! ]+ h: |, d2 \
6 ]" ~+ u( f/ b' h. s- v @Value使用步骤; z. c u) h# E
# f+ ~1 _1 A: N( S 步骤一:使用@PropertySource注解引入配置文件
( g; j. V) s4 B. u" x( m' M9 c% d
7 Y- a) y) r! G( Q 将@PropertySource放在类上面,如下
- m$ F. u) W' h' J- g- ~ O9 i/ W/ \
9 L7 l6 v- M: k7 z @PropertySource({"配置文件路径1","配置文件路径2"...})
9 i6 b$ q6 e4 m) m6 y @PropertySource注解有个value属性,字符串数组类型,可以用来指定多个配置文件的路径。% l1 _8 v S B- v; |9 q; B
+ I: L p, y+ D# a$ [- R 如:; Z% I" i c5 k1 E) n0 O
5 s1 \8 |% H. z& C @Component
0 ?, T' ]8 J& a; } e5 c @PropertySource({"classpath:com/javacode2018/lesson002/demo18/db.properties"})0 y+ ~4 ` V k0 j2 P! B: r6 t
public class DbConfig {
6 R& j7 b- h+ M& X }5 z" O" p6 _0 `6 ~8 P# P. x$ a0 _8 X
步骤二:使用@Value注解引用配置文件的值
_ o; _# \4 Z$ z$ y R0 Q
& f5 }0 ]' \7 z$ B1 S 通过@Value引用上面配置文件中的值:
: O( \7 o+ e8 t" ~8 |. [ 0 z& H- U% m, Q) ?6 [% m Y
语法
8 Q ^! d0 ]3 b 4 j4 p* V$ P* L5 ]
@Value("${配置文件中的key:默认值}")% |4 O! f4 g$ ^% ~3 P) e
@Value("${配置文件中的key}")
! C3 G8 \1 w& \# _: a 如:
( O. P2 {. A5 z1 h7 h& x9 S% D - z L- K( w/ W3 ^6 {# w5 j
@Value("${password:123}")
_5 h: r3 ?6 [( Y 上面如果password不存在,将123作为值
4 y1 y' o# b! x- ` , R8 e' K9 S* n0 _. G9 L0 T
@Value("${password}")3 z, Z+ I( j5 T( q
上面如果password不存在,值为${password}
0 E9 q+ z- E! u0 o: y! _6 g # R5 \. @+ Y" p
假如配置文件如下
- ~' E, W/ T! g' b, k3 T' G1 y- R7 Y o7 t' C" ^* q- t" Y" A$ _
jdbc.url=jdbc:mysql://localhost:3306/javacode2018?characterEncoding=UTF-87 }+ g$ W) R9 b9 t3 c) u
jdbc.username=javacode
1 S: a; V4 z2 r. H, p jdbc.password=javacode8 ?3 O: J0 C# n4 ?
使用方式如下:% r. j3 l* n8 K H! r+ Z9 M
1 |7 c$ i) }) f- } @Value("${jdbc.url}")0 [: Q, o* z! c; H
private String url;! S& S: C' ?& r2 g
7 ^9 R9 E: h$ I4 j
@Value("${jdbc.username}")) \3 b+ l+ A% O7 T( x7 ~- ?8 y
private String username;& b0 H9 M* e: M3 J
; i$ x* I( I% [2 q
@Value("${jdbc.password}"): i7 A2 p# y6 ?; S! O9 @" s
private String password;7 |$ {5 m4 `; p6 t- o# O
下面来看案例
! d; J, x7 q. s H3 m4 }, L + [* O( l4 u% A z" q' X
案例
9 \* ]* i3 {7 j# K; e4 P: _
6 d# u' E' v/ k/ l 来个配置文件db.properties
/ j6 l9 D R; N6 D. x* l ! U, D0 U; ^. M/ S, q% M' n
jdbc.url=jdbc:mysql://localhost:3306/javacode2018?characterEncoding=UTF-8
5 {$ h" t$ u8 ? jdbc.username=javacode
9 q, H' }$ x- h, O1 c8 k jdbc.password=javacode
' T4 v8 T0 I2 `# E0 C 来个配置类,使用@PropertySource引入上面的配置文件! A1 j1 Z k. F/ D# H% P8 \
4 t5 T1 W3 G5 l) U- I package com.javacode2018.lesson002.demo18.test1;% q. s5 Q: L" s7 S1 F6 g
" \, V; d) C8 d: \! i
import org.springframework.beans.factory.annotation.Configurable;
: e: M, |0 U9 P% s b1 l import org.springframework.context.annotation.ComponentScan;& l2 a" ~5 T4 K( N5 C( {% N: W
import org.springframework.context.annotation.PropertySource;2 r Z8 U+ M) I( }! Z! f# Q
6 }3 t2 Y. T$ h- ~6 R+ P" W @Configurable
8 z8 k ?1 x* w: h9 a2 `. a @ComponentScan$ U g6 A: J# F2 C7 j1 }* j0 O
@PropertySource({"classpath:com/javacode2018/lesson002/demo18/db.properties"})
- g; t: Y J2 B- J; `" Y public class MainConfig1 { c0 m5 x! p0 \ ^
}
`% B" A; a- o" m 来个类,使用@Value来使用配置文件中的信息
) z- W) }8 ]0 n# L5 w
, |+ }5 Y/ O. `- W5 ] package com.javacode2018.lesson002.demo18.test1;
- L( G: P5 \9 W8 w5 X4 Z8 q% [
7 z5 u; y3 G( s$ C# C/ l import org.springframework.beans.factory.annotation.Value;2 T. R7 c$ J( a1 c5 Z! n
import org.springframework.stereotype.Component;
9 p& j; X, j5 o
$ L8 r: V" K- G# k @Component
% g9 U6 O% p# C( A! A public class DbConfig {
* _2 ]6 P) K3 s. \6 I
) q% l9 T$ f9 @; m. Y/ S @Value("${jdbc.url}")
4 A4 H' s! w9 d7 i3 v private String url;/ F z) I z2 w+ H* B9 E$ d
* M, C, W' j0 T: e: E5 e9 L' r
@Value("${jdbc.username}")6 y$ k$ m) A5 v8 j/ T
private String username;; V: \4 k9 k! s! P! w+ m
; c9 U1 G; ^/ _0 h# M; A+ b) ` @Value("${jdbc.password}")
4 I J* d5 H/ z' H% z8 q private String password;$ M: M: }5 `6 k; E5 s
% G; S7 a1 Q3 x3 z, P/ @! u
public String getUrl() {
/ I5 X% b- S% k+ I i! w+ A, F9 Y return url;
/ b' Z0 Y6 V, A, U5 {( ^ K }
- b! \1 S% y" l6 E m
4 ?# L; g' i& i" G public void setUrl(String url) {
0 [! d. U4 t( W' S9 z# Y2 u this.url = url;
( z5 k# M5 V/ g7 r. a0 j9 s/ u/ r }5 }) c% D. s7 O
5 G3 o% O+ _ f& Z' K
public String getUsername() {) m8 U& O' F/ H, u8 ]
return username;9 M/ z3 u$ [$ D8 x
}4 G3 {- C3 v, F! T7 O/ }, P/ {
% X [/ E& x& X6 C" |8 u$ [% T public void setUsername(String username) {
; u' _$ `, ^* E; T this.username = username;
8 m- r; D* q- u }* T8 }& m1 ]' O
7 c, P1 |5 ~5 ~7 x public String getPassword() {
; F+ v5 l" \5 Y, h) ?0 o3 I9 j8 Q6 v return password;) E/ [& R: f5 f$ ]
}
; j! t: ^* r# J2 d3 h* N' z ) `, W3 J1 o3 ?! H5 x4 Z6 Y' U
public void setPassword(String password) {3 u! ~2 s G4 F: @& I7 f) }, W
this.password = password;
( k; [: _/ i( ]' e- U" d+ z }
: s# B7 Z- C( b- A * n: R( t" u- C- X& k
@Override1 Z4 ]1 I6 b( @# f7 g$ }" |/ H
public String toString() {
$ ~# y; W; x9 [9 f ^- K/ G7 u/ Z return "DbConfig{" +! V. ~% e, C' L7 k0 X) o7 J3 q
"url='" + url + '\'' +) t: m- }; F; Q, T. T4 R. \* e9 J U
", username='" + username + '\'' +! |, n( k' r! l+ t
", password='" + password + '\'' +
% ?& {* }) w7 u) m '}';3 E& n7 T/ k+ y4 p* T5 q/ K! o5 V
}7 v1 i: |! n( w3 X& B; K) j
}% K% |$ H; N* R' d+ n3 _
上面重点在于注解@Value注解,注意@Value注解中的; I, y t3 O" C: d
$ ]/ s) p" n6 K+ _# ?+ S" f0 q5 Q2 z
来个测试用例6 j# \8 Q8 _% Z, a1 U
7 W2 y" `) U3 a8 P% v! O; P# ?/ K' Q
package com.javacode2018.lesson002.demo18;
$ M [: t: C- ~+ d; j& A$ K* Y" x ( q/ p: W9 c6 V; A2 v4 H$ J3 R
import com.javacode2018.lesson002.demo18.test1.DbConfig;6 F9 z E, U/ ]+ A$ \ Z
import com.javacode2018.lesson002.demo18.test1.MainConfig1;
. Z! K/ k( }" e import org.junit.Test;- d/ l1 `3 W# x; { \; b# I
import org.springframework.context.annotation.AnnotationConfigApplicationContext;2 p6 {# T, U1 u3 @6 J" r
M3 r: t! H4 z! w9 }7 W5 ^. E
public class ValueTest {
( W' d7 T2 ~) x5 a- o/ k% s: t * K+ I8 j' @, M) Y( K4 J
@Test
, H( p" j# Z2 z. `4 d public void test1() {
2 Q' Z0 z% m9 V" C! O; | AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
8 s% D: Z0 \! V% A$ M context.register(MainConfig1.class);
3 n& `) m4 r. L2 E# I context.refresh();
& p* w* @ G* r8 D6 h 4 h1 [8 @* s" K( @ s7 o3 e( U/ w
DbConfig dbConfig = context.getBean(DbConfig.class);
8 G- i9 N# ]1 P! i* e! {0 r! O System.out.println(dbConfig);
7 j9 J/ J2 c$ l+ ]$ [. s }
# V, W+ r3 w8 k, J: _% e }$ q- J% p' P$ T$ F$ {3 y# s3 M
运行输出; Y+ @* P) \8 Q% f2 a
1 ~" f: v' }' q3 Q* h
DbConfig{url='jdbc:mysql://localhost:3306/javacode2018?characterEncoding=UTF-8', username='javacode', password='javacode'}
# T" {6 r# b, n% v9 O! n4 J3 K 上面用起来比较简单,很多用过的人看一眼就懂了,这也是第一个问题,多数人都是ok的,下面来看@Value中数据来源除了配置文件的方式,是否还有其他方式。% l( W" [4 k; S* ]; Z- {) f
. A9 o v! ?; l; K( @+ k @Value数据来源3 Q0 i1 g- |7 G5 H+ V3 d2 _" n
; V( m! b) g1 l 通常情况下我们@Value的数据来源于配置文件,不过,还可以用其他方式,比如我们可以将配置文件的内容放在数据库,这样修改起来更容易一些。6 T. ?; P9 R; ]- T
' O( J- x* ^+ ~( i 我们需要先了解一下@Value中数据来源于spring的什么地方。5 i+ b1 q* e4 ^' x4 `% U
& w- G/ g. }( m8 e! f* q$ e; |
spring中有个类" d6 c3 C& m4 o# z# e4 I n2 E
5 t. K% G7 \& V% d( [- z4 Q3 F org.springframework.core.env.PropertySource
" D2 p" i4 a2 u# _ b+ E 可以将其理解为一个配置源,里面包含了key->value的配置信息,可以通过这个类中提供的方法获取key对应的value信息
. x( b- O5 C+ D' z- [; G/ a/ J \+ A
. e) A/ A& M$ y& Y5 v+ Y" w. G 内部有个方法:' N% T# b2 H( n+ G
5 v( _1 c! N* @# p$ g
public abstract Object getProperty(String name);6 y9 m% B$ o- b' `: W( @
通过name获取对应的配置信息。
0 u: j: `; h+ `/ R8 y% m
; @% M* V# }; \ 系统有个比较重要的接口
, v3 `7 f$ T! A 0 r2 b" Y ~: x7 X) k
org.springframework.core.env.Environment
0 y: A% n( a' l4 ? 用来表示环境配置信息,这个接口有几个方法比较重要7 o3 R9 J4 K: ~/ P( K
, p" q# J4 _0 j9 b0 O3 Z+ `
String resolvePlaceholders(String text);+ l6 N1 g8 j) \* `
MutablePropertySources getPropertySources();8 ?- {7 z$ C+ V" o, m4 i
resolvePlaceholders用来解析${text}的,@Value注解最后就是调用这个方法来解析的。7 S$ ^, U' w3 G1 `5 T
/ _, [8 h* _- B" J! j: F! A* G
getPropertySources返回MutablePropertySources对象,来看一下这个类8 a* ?: Y3 H9 K
/ k- M( o m7 { public class MutablePropertySources implements PropertySources {0 y4 w5 E8 H# n! p: a" s% C
0 k, z: Y! }% L! Y2 H1 l% h
private final List<PropertySource<?>> propertySourceList = new CopyOnWriteArrayList<>();
% v- L. k% `1 g, {* e/ n $ k* \" i0 h6 k5 n2 E8 I
}8 |2 d! j4 l+ A9 H
内部包含一个propertySourceList列表。* s8 Q: D3 @& {+ U, l
7 k n$ r6 | H2 Z9 Q" H- M spring容器中会有一个Environment对象,最后会调用这个对象的resolvePlaceholders方法解析@Value。* R9 O2 o. e' E7 }6 O" ]
* }! H' A. g* u0 ]7 O 大家可以捋一下,最终解析@Value的过程:
3 N1 k; s. F3 X$ s
* z5 K4 I! p: _+ G7 ^9 s% o 1. 将@Value注解的value参数值作为Environment.resolvePlaceholders方法参数进行解析* H: e# g4 D2 h3 b
2. Environment内部会访问MutablePropertySources来解析0 N: P0 B! g- j* O2 p1 A- k. [
3. MutablePropertySources内部有多个PropertySource,此时会遍历PropertySource列表,调用PropertySource.getProperty方法来解析key对应的值$ _) F% M" l$ E( f' x2 P5 Q% H) ^
通过上面过程,如果我们想改变@Value数据的来源,只需要将配置信息包装为PropertySource对象,丢到Environment中的MutablePropertySources内部就可以了。
4 }+ n* `4 x! [ J& N6 [: w5 \
8 M% E. c" k3 U8 P" m& p2 [1 e 下面我们就按照这个思路来一个。$ e3 z# o- K8 S. O u
5 U: v& v6 R; J( e 来个邮件配置信息类,内部使用@Value注入邮件配置信息
/ p+ o% I/ S5 P5 @, y
+ s/ Q3 n( T% V: U q package com.javacode2018.lesson002.demo18.test2;9 r1 r: E8 h8 s' e8 x5 x
8 Y( g; J6 _2 J. m M j$ `
import org.springframework.beans.factory.annotation.Value;$ ?1 D" U; ?$ N, }* B. L: \- I
import org.springframework.stereotype.Component;
6 o! n% {: ?3 n/ _ F6 l
& Q# @ D' Q" e7 ] /**7 z1 {6 A) M5 q3 {; ^, b6 Y
* 邮件配置信息$ r# w5 n# C2 t1 |# \: U) M
*/5 k) E( b* J3 Z# ~
@Component
2 h3 D" D) \4 l0 G+ H public class MailConfig {
7 N9 v7 M' h `1 H/ F
+ g$ p6 |% R" i4 }* u1 D4 X, M# ~/ C @Value("${mail.host}")
9 H& M! S w( q3 }; O7 h& }9 E& }" M private String host;$ h' s" ~' J* u9 y7 N0 d( }
3 C6 }8 h7 B& F- K4 z6 G A$ q/ N: a4 {9 w
@Value("${mail.username}")* Q! E; g2 _+ n) E% J4 m5 s
private String username;7 L' a7 L5 E/ b% L {9 U' R- `
" R' ]1 D0 y& t6 [( Z3 |; E I
@Value("${mail.password}")2 \- }- N, ]" {: ~
private String password;- ?/ A$ e+ E( c
+ i6 _3 B5 j6 w6 A% Z7 E8 Z0 y public String getHost() {
+ @3 y1 m" b$ j2 K Y6 r& | return host;* l0 t, S" T4 z6 [& B, {0 {) n1 Z
}
( l- ]" A) P1 R7 l
$ k1 J; R- t: Y" Q" |* x7 C public void setHost(String host) {& B0 ^* B9 w3 k" \/ u0 x
this.host = host;
+ k# p- _2 O. `! J- l; K/ p }; b: `+ r; x2 ?) K# ~
6 q4 @+ V% z" d3 z$ @. ^8 L$ x% r1 w public String getUsername() {3 y' C3 o7 R/ n8 i7 f
return username;
% v4 I7 h: w6 k L+ l$ K' {0 x }( Y) c4 ^; b- a2 z! D3 C
1 @3 Y* V U9 B, S1 c$ [ public void setUsername(String username) {
: X: q7 p3 @/ R+ q! J3 x this.username = username;
( b8 k/ J3 ` z5 F7 T }
3 s* ^" Z( x. U$ D7 O
6 |5 ?, E2 ^2 U7 |4 g public String getPassword() {
" g/ O% h- ], M) H4 z8 K9 _ return password;8 S% b) P) S A7 n: e
}
3 X* S, y; Y5 q# I 2 u) V% ]7 T. `- O9 ^
public void setPassword(String password) {1 d, c$ p& q3 Z; R( w7 L. q6 }4 r% F" W
this.password = password;: A; c* N2 o) O S
}
: S# }/ d" ~0 z+ [8 n
o6 [3 Z r' D; {5 a7 U+ K/ g* {6 L& D @Override% x' q! v/ \! v- J7 i
public String toString() {8 C- L; C: ~2 P3 P' }' L7 t
return "MailConfig{" +
, P& b: ^5 s9 x: ~5 F "host='" + host + '\'' +
( ]: u& S- s6 j ", username='" + username + '\'' +
. P: _. g+ N9 O* @$ u9 @ ", password='" + password + '\'' +2 h9 _& r8 {- d( `5 l. _
'}';6 O0 ?. z' F+ n% e+ U9 m3 }
}
" ]) J5 J1 q% \! D3 m0 N6 K }
7 J9 f# U. W! ?) h& t 再来个类DbUtil,getMailInfoFromDb方法模拟从db中获取邮件配置信息,存放在map中" q! ~3 r4 D; v1 k( D/ E
, a0 [# Y+ Z+ D) u: T
package com.javacode2018.lesson002.demo18.test2;# I: Z$ V7 o+ ]0 M8 X! ]: O
9 O$ |1 H! J7 O {! [; [7 J
import java.util.HashMap;
/ r ^! ^ r0 G( t/ z import java.util.Map;
2 v5 l9 C7 a$ ~/ |
+ {& L' _+ j* @# W/ r2 |, p7 U H public class DbUtil {5 N/ N4 \1 i5 c" z8 D# y' P
/**5 U1 x: Q0 O- f& t- @
* 模拟从db中获取邮件配置信息
# R: N4 U A, W *6 Q5 F$ O, o- }1 @
* @return' a% U6 h- z. a8 F7 v& H& i
*/
J$ u A! C1 V& @* K# h; J public static Map<String, Object> getMailInfoFromDb() {
5 c( Q) S( k- y) F' p Map<String, Object> result = new HashMap<>();
- m; O2 w% J& x) p( T result.put("mail.host", "smtp.qq.com");
9 N; u1 Z3 m6 C |6 }' n result.put("mail.username", "路人");& l5 [2 a# M: J6 S d( \% H, y
result.put("mail.password", "123");
8 R5 b3 i$ m* ?7 |6 M return result;& ]/ P1 B- y0 S8 l& r
}1 x1 G" a: C- i$ @
}9 ]. K# f+ B" ]7 k2 O5 l
来个spring配置类
/ D/ i/ \# J' s% M7 [, D7 \8 _
0 m! c% p( w% F+ s6 ` package com.javacode2018.lesson002.demo18.test2;
, `* t T A+ F2 `
5 n6 I- ]) d0 `! r! Y: F import org.springframework.context.annotation.ComponentScan;
* t, s* @: c7 w# I* | import org.springframework.context.annotation.Configuration;7 O$ U0 b$ ^1 I* H
) t1 l7 L! y5 P @Configuration
: h0 \) J7 V l1 z: J @ComponentScan
8 _2 w- T6 a) J0 d public class MainConfig2 {; a' X' v% K5 w6 l* X
}
- K' R( W7 g P8 Y, j& v; [* ? 下面是重点代码% q9 l0 l! G4 X# R
0 p. v) v$ I0 f/ |) l6 f
@Test8 K4 K" N3 Y) Y. k! v
public void test2() {! R+ G3 N4 d+ _
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();0 H( O4 H4 Y6 C% ^
6 u ~5 p9 x" P4 @: V
/*下面这段是关键 start*/5 n3 n6 _, C0 S) l3 p
//模拟从db中获取配置信息9 K. c3 k, `2 w3 W/ I
Map<String, Object> mailInfoFromDb = DbUtil.getMailInfoFromDb(); R/ o' m/ E% f1 b) H4 u* k
//将其丢在MapPropertySource中(MapPropertySource类是spring提供的一个类,是PropertySource的子类), @* g! f! F/ \, P/ \& Q% R) S: J
MapPropertySource mailPropertySource = new MapPropertySource("mail", mailInfoFromDb);! Z6 ?! \$ }( U, a0 h0 u% M$ ~
//将mailPropertySource丢在Environment中的PropertySource列表的第一个中,让优先级最高
) v3 W8 l! D: z# a5 z" D context.getEnvironment().getPropertySources().addFirst(mailPropertySource);
% X: C$ F) U; S( R$ _' E- e /*上面这段是关键 end*/
9 P! B& b! B) y$ a. J; o
" V; n# m- f1 b2 \' J context.register(MainConfig2.class);6 ?# O& |1 [5 U1 `6 x
context.refresh();
* A" h n( ] h1 X MailConfig mailConfig = context.getBean(MailConfig.class);3 U4 K0 M1 e( C" d k
System.out.println(mailConfig);
. x& Y; @+ d6 L0 @, o0 c, B& X }4 [0 X s# S- A' }
注释比较详细,就不详细解释了。! ^0 g4 j) A0 ?- k! R. e
# d- x5 d. B" b9 z2 R% V) ]
直接运行,看效果4 v+ z$ T1 a3 ^/ e$ G8 n$ y+ Q- {
8 e5 K I# ^5 A" C# _ MailConfig{host='smtp.qq.com', username='路人', password='123'}( }' Q$ X3 X3 |4 `
有没有感觉很爽,此时你们可以随意修改DbUtil.getMailInfoFromDb,具体数据是从db中来,来时从redis或者其他介质中来,任由大家发挥。& E* R0 Y# }; [) x7 A
) K2 Y. S! x3 L. c s, B( _2 ^ 上面重点是下面这段代码,大家需要理解, |: s4 ?; Y, Y" J: P
! a& }, X8 O9 N) p9 ~+ L& i
/*下面这段是关键 start*/2 O' G) T n4 v$ \
//模拟从db中获取配置信息
' A& { R* h. N2 x# O3 ^; P Map<String, Object> mailInfoFromDb = DbUtil.getMailInfoFromDb();
; _2 K$ {# G3 l; s5 f: G% a% y //将其丢在MapPropertySource中(MapPropertySource类是spring提供的一个类,是PropertySource的子类)5 x/ U6 a( v# P8 w' K" p6 t- L
MapPropertySource mailPropertySource = new MapPropertySource("mail", mailInfoFromDb);' R9 f! h' _2 G8 ^) g
//将mailPropertySource丢在Environment中的PropertySource列表的第一个中,让优先级最高
! ~4 U' Z( B$ ^% M3 L- d: C* N, d context.getEnvironment().getPropertySources().addFirst(mailPropertySource);' Y( p5 J8 ^- Z# M$ @( g
/*上面这段是关键 end*/
' ?, _1 c: T& ~+ o 咱们继续看下一个问题
) R0 x0 ` ^; [1 ?( } * [3 D) F6 F# Q- W+ o. N0 }
如果我们将配置信息放在db中,可能我们会通过一个界面来修改这些配置信息,然后保存之后,希望系统在不重启的情况下,让这些值在spring容器中立即生效。
$ @7 Q5 _ o' z- [+ j
9 a" D/ i% O/ P @Value动态刷新的问题的问题,springboot中使用@RefreshScope实现了。
+ F- e3 N6 ?1 K; Y* G) _ d m$ J. x* d
4 D5 o: U# K- A7 I P3 \ 实现@Value动态刷新
; q! f* b$ P# h! v" A/ `! ^ 5 {$ _- F7 D6 _! ^! ^1 V
先了解一个知识点3 S8 h% H: l& e& t; g$ e& b
! q( Y* J/ x# I4 T i1 f 这块需要先讲一个知识点,用到的不是太多,所以很多人估计不太了解,但是非常重要的一个点,我们来看一下。6 o9 P h- i! w) G
" Q7 } o# x% }% V( p/ M% Z 这个知识点是自定义bean作用域,对这块不了解的先看一下这篇文章:bean作用域详解 e9 M) S B$ F' l; k9 A# F
0 a) n! O* K* _ bean作用域中有个地方没有讲,来看一下@Scope这个注解的源码,有个参数是:0 c9 R6 |2 s0 D* I. e
8 t! Q' @; b1 Y% \2 D ScopedProxyMode proxyMode() default ScopedProxyMode.DEFAULT;& w1 ~8 A. H, c3 X
这个参数的值是个ScopedProxyMode类型的枚举,值有下面4中, q% B: ]/ y* O
0 j! V9 i# Y8 y/ P
public enum ScopedProxyMode {
# c* S: N' n" N DEFAULT,) e# {/ ^* N" P4 t2 ^
NO,
8 p b2 S* ?. I! ` INTERFACES,
7 S, I2 l/ v2 U! V7 Y1 `. w TARGET_CLASS;
9 z/ O; Y% ^. [3 n: `& o; `5 } }" E1 l& ^8 j2 |
前面3个,不讲了,直接讲最后一个值是干什么的。
: t4 m- T( ?1 w& @" a" z/ _& s+ ^
& B0 `! i2 _- J( k 当@Scope中proxyMode为TARGET_CLASS的时候,会给当前创建的bean通过cglib生成一个代理对象,通过这个代理对象来访问目标bean对象。. ] o- c+ T+ R+ w, R2 S+ }
/ c1 m- G4 r4 \ 理解起来比较晦涩,还是来看代码吧,容易理解一些,来个自定义的Scope案例。+ c. ?: I. Y7 P' S n
: t6 U7 N/ r4 a5 a; s" E 自定义一个bean作用域的注解. r' ]0 O4 L0 F" a4 l8 S0 a5 ?1 N+ g
; I: \. `' A/ { U- _$ L
package com.javacode2018.lesson002.demo18.test3;4 r7 U0 b2 K2 f; e4 Y$ G
+ c; a1 Y5 k! S! b6 D& i2 J( t& x import org.springframework.context.annotation.Scope;
$ R2 m2 M) B- J. n import org.springframework.context.annotation.ScopedProxyMode;
' _ M" r5 p! |( s 8 ~9 n/ |- ~3 T4 o& H* `* I# i
import java.lang.annotation.*;
0 A' J6 \$ B" `3 U, V S 7 O' W1 ?, n# F
@Target({ElementType.TYPE, ElementType.METHOD})
" u% ?$ w. U7 q1 T& k* _8 b! g @Retention(RetentionPolicy.RUNTIME)
6 C! d2 g8 h% m4 h8 E; c @Documented
, z% \) Z9 ~8 n/ g; B) N; {2 F9 o @Scope(BeanMyScope.SCOPE_MY) //@16 k* i: c, B( \4 U5 A
public @interface MyScope {
4 n1 ?7 I& p `$ S" F: W /**7 |: Q% v3 P9 S3 q! ]/ K
* @see Scope#proxyMode()5 }% ^* \0 |9 G! n) Z1 u+ b( C2 }' X
*/* v' F8 {7 o6 `7 u5 m7 u7 w- H, a
ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;//@23 Y- { g# F1 \6 V3 |
}, } r! m5 x' \8 n1 y6 U" ~. r4 s( t5 H; k
@1:使用了@Scope注解,value为引用了一个常量,值为my,一会下面可以看到。6 Q6 T8 }" v& s! x9 `
5 s0 Q- l5 `# S) L4 ? o2 W" c
@2:注意这个地方,参数名称也是proxyMode,类型也是ScopedProxyMode,而@Scope注解中有个和这个同样类型的参数,spring容器解析的时候,会将这个参数的值赋给@MyScope注解上面的@Scope注解的proxyMode参数,所以此处我们设置proxyMode值,最后的效果就是直接改变了@Scope中proxyMode参数的值。此处默认值取的是ScopedProxyMode.TARGET_CLASS, O) U. Y9 z% G2 a: H" z! D% _- l
. q9 d+ a( O! M& e# p9 H
@MyScope注解对应的Scope实现如下
8 D, d4 r! H6 w$ {4 {, V, A " t3 @) k7 Z. V4 F0 O
package com.javacode2018.lesson002.demo18.test3;/ z- k- |$ K+ l0 I& y- b$ p
8 q% Q. c' R2 q S0 {
import org.springframework.beans.factory.ObjectFactory;) o, B/ b8 z1 ^5 r& `6 @( _$ h
import org.springframework.beans.factory.config.Scope;1 P/ O6 B5 h' l) w' H* r, e
import org.springframework.lang.Nullable;0 H2 A8 s2 `6 ?( S2 g
0 L8 G" r/ M2 A /**/ u* |8 L u4 S. \! g3 c
* @see MyScope 作用域的实现
; J1 @! K- h0 M# x: A */
+ I- W) V' C3 L' Q+ f public class BeanMyScope implements Scope {
& ~) E/ k7 N f0 W
0 D% P; ` G2 d$ i& i( Z0 s; ~5 U public static final String SCOPE_MY = "my"; //@1' {3 ]- x8 j+ t/ o
& {% l1 z+ g# l! r$ [: ^ @Override
, A, }& K a t1 E9 W public Object get(String name, ObjectFactory<?> objectFactory) { & H" f+ D: e3 I9 N2 L
System.out.println("BeanMyScope >>>>>>>>> get:" + name); //@2# i- y6 W! c1 o# Y& k k
return objectFactory.getObject(); //@37 \- E6 v1 b- q5 z+ y" G* r
}8 f+ N8 a( a# Q. X
- p% C5 Z* {3 g* V" `4 |' U @Nullable7 k; ?9 I" b/ H7 C5 W! G' t
@Override$ N" n& k; n! u
public Object remove(String name) {+ H/ x+ S8 E! j5 }
return null;
1 u Y; L/ t9 o/ Q9 H }
w4 p' f6 v1 Z! o# j
3 Y- \! U+ O+ W- y3 d @Override
' i% A, d8 B* W/ c! N! m public void registerDestructionCallback(String name, Runnable callback) {
3 A& k2 V R) Q5 q/ W( S
; j! b; k% [. d& X# n% [ }
" R S3 u0 E. i6 C+ J- e
% u$ s- G3 x j* | @Nullable6 t/ Q9 F- C$ n8 f
@Override. w2 x) Z. x$ q& Y+ ~( O
public Object resolveContextualObject(String key) {
+ ~% Y7 w3 ?5 H0 l6 B& e9 g+ {+ J return null;, F. ]$ x/ j+ X1 P; F
}
! R. p- T+ `# P$ Y7 R5 j2 J
7 Q* t6 c9 {* O0 y @Nullable
' F7 r2 v7 }7 `! m @Override
" @* O) d; y E8 M/ | public String getConversationId() {
, g' S+ a; o! @9 T2 p4 Z& @ return null;6 c& S) x# W# _$ V
}1 p6 t5 @% c: N& I4 u! w# U- F
}% }9 \# V2 [2 S4 F9 z6 n3 a ?
@1:定义了一个常量,作为作用域的值
# a/ {; v9 `; k/ \3 l, i& t% E4 w & i- \) E1 {5 G
@2:这个get方法是关键,自定义作用域会自动调用这个get方法来创建bean对象,这个地方输出了一行日志,为了一会方便看效果, \: E# M6 g- a& F; t
* G+ F4 X3 a/ W7 ^ @3:通过objectFactory.getObject()获取bean实例返回。
2 A! p* C; I* Q8 T$ E5 V
8 ?4 z3 {8 S: K; o" f4 C3 ~7 q, } 下面来创建个类,作用域为上面自定义的作用域5 i' M$ @/ {% }) p) `# F1 H
: B _( i1 h. z$ ]8 O
package com.javacode2018.lesson002.demo18.test3;1 E4 R" r7 B8 Q) a+ n3 ^$ d
# R7 e$ z3 l" p
import org.springframework.stereotype.Component;, `8 O6 R9 l! Y1 O" L/ E! M# M0 L* R
2 z6 G+ c! w7 p0 A' S) L* r) J
import java.util.UUID;* H0 s8 ^! Q8 X- z" J! g9 i
/ q% M5 |% r1 P' Q @Component7 U! i* W$ x" _, j% k- t
@MyScope //@1 & @. [4 N7 ]: z' c9 X& h, r
public class User {# M' g" D/ F! s3 R% t
& ~; U) f3 {: ]% ]6 U3 g# R9 z
private String username;
$ n, ]9 c" \% c , h8 p5 L9 x7 O3 |6 {8 R
public User() { 3 j$ A" N: f0 c. X5 v8 t
System.out.println("---------创建User对象" + this); //@2+ d+ I2 s# D4 Y( E6 f
this.username = UUID.randomUUID().toString(); //@3+ M: V6 r' O! }. F' z5 l
}
~* T' v7 {2 c+ B0 ]. z$ M8 h ) n: @- G c0 L
public String getUsername() {, n! _+ X& k7 B! T' V
return username;
' { c" g1 O+ m/ P4 S7 H }
' L$ ~+ ^. a9 ^
; x; X* k2 Q1 K7 g$ F" p/ [- V, X public void setUsername(String username) {7 W# H% K( \5 u2 {5 K' ?
this.username = username;2 _% @1 h0 l& e$ T; J/ |
}0 E0 e# |; m0 Z8 O) Q2 M
y/ T% r3 Q* O9 Y7 f% h4 [* x1 h, Z9 T
}
( P8 D: L: E7 F& L2 } @1:使用了自定义的作用域@MyScope
# c; j# k: y) w- J
3 i2 \" S3 g: \4 M @2:构造函数中输出一行日志
8 h6 x3 `. o0 Y1 t# J
) B7 W1 V! Z4 o6 J4 d, n$ b @3:给username赋值,通过uuid随机生成了一个# W4 Z$ A! A/ B% }, e, c4 e f
: I4 |% @! @- T& w4 h! @9 M
来个spring配置类,加载上面@Compontent标注的组件
5 d4 {0 X* I* d) H* _' _ 4 n" Z M0 l9 v8 L
package com.javacode2018.lesson002.demo18.test3;- u8 J. x( u0 h6 {6 S/ U$ r8 R
: p5 Z6 F/ Z) i+ M. A4 v: t import org.springframework.context.annotation.ComponentScan;! K$ Y" ^7 E/ i# b. E
import org.springframework.context.annotation.Configuration;# Y+ k+ a: Q. \7 x
8 L* m' s3 n+ E: r' l/ H @ComponentScan
+ V" t5 ]1 w' X: t @Configuration
; K* m1 ~/ Z/ M, o! J. H2 l; l% [& n public class MainConfig3 {" n: _/ {$ ~( O$ m- Y& [
}
) f: B) N0 W7 L- Z! _ 下面重点来了,测试用例 a6 n- U. J( `
6 r6 n- d$ ^" }' k5 ^) r @Test( G+ M/ u' J0 {- M! u" M X
public void test3() throws InterruptedException {" W H6 D8 P/ S3 k( i/ }3 d9 C
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();9 {9 A& m; j( U) a9 V
//将自定义作用域注册到spring容器中" a0 a' J/ M1 b2 W5 w0 t6 D; {
context.getBeanFactory().registerScope(BeanMyScope.SCOPE_MY, new BeanMyScope());//@1
% z5 i. W7 F, ]4 ]2 D context.register(MainConfig3.class);% }( z1 d% v, U0 j% O
context.refresh();5 _' k# p% [2 w' {! L& n/ l! o
/ S: e3 q8 Z: I. n5 M! |- ?, H2 e/ L
System.out.println("从容器中获取User对象");$ }7 m9 y3 y6 u
User user = context.getBean(User.class); //@2( ]# |2 @* t' P4 F. v: x8 f, y' s
System.out.println("user对象的class为:" + user.getClass()); //@3
8 c4 l, J1 |3 f) B
6 O$ a- T/ D5 b9 ?9 u( I! F( S System.out.println("多次调用user的getUsername感受一下效果\n");
5 V6 i) Q; F- N6 F% ?# I+ f for (int i = 1; i <= 3; i++) {
1 ?8 S- v3 O5 C. f; | System.out.println(String.format("********\n第%d次开始调用getUsername", i));
9 M. ]8 r( T: _% \! n: c System.out.println(user.getUsername());
& L, g( ~/ a6 k/ m$ K! T; H System.out.println(String.format("第%d次调用getUsername结束\n********\n", i));
+ l5 D3 P& X# p1 s- r }
$ N8 F! S; j' V, f S3 }* d }
; L/ X/ |3 X- [- [ @1:将自定义作用域注册到spring容器中
) o8 B$ T' z0 J6 X / g ^: R1 ?3 i) x1 G
@2:从容器中获取User对应的bean
4 K/ E& G1 H+ N( N d A & W' y/ Y% q8 t
@3:输出这个bean对应的class,一会认真看一下,这个类型是不是User类型的
2 a) z* I3 ^1 m4 p2 ~) i
# y8 N* r6 X7 y9 P! l$ S 代码后面又搞了3次循环,调用user的getUsername方法,并且方法前后分别输出了一行日志。1 J9 g7 S4 ^6 T; f! r
* P0 n" a5 I6 f U9 f
见证奇迹的时候到了,运行输出' r c6 T9 C$ W
8 ^2 v2 t* p8 ?$ `" y6 ~ 从容器中获取User对象
* |1 E, f; i% z3 ?* z( k9 W user对象的class为:class com.javacode2018.lesson002.demo18.test3.User$$EnhancerBySpringCGLIB$$802331271 U# Z2 ~& f( X! B" A
多次调用user的getUsername感受一下效果$ a9 y/ T7 n* t: d
9 Z& G* o% R/ t" q" g5 a7 `
********
, J7 h+ B0 b- b0 z/ ? Z 第1次开始调用getUsername3 y. M; t6 w6 I0 {0 w; h4 _. x& X
BeanMyScope >>>>>>>>> get:scopedTarget.user% K$ [: t1 F8 W& v$ w% K2 t/ x
---------创建User对象com.javacode2018.lesson002.demo18.test3.User@6a370f4 z7 X+ d# M$ _* R4 Q* O, n! b
7b41aa80-7569-4072-9d40-ec9bfb92f438
- D+ b- x/ f, ]) h$ H 第1次调用getUsername结束, O2 f, w: G/ F
********$ J( r9 C9 G3 }9 l9 `
0 X: u1 V8 C6 i X+ _ ********) ?$ @9 t. K) C" t
第2次开始调用getUsername- B* \9 M, E6 j+ d: F
BeanMyScope >>>>>>>>> get:scopedTarget.user
+ A" U G. f2 u ]; p ---------创建User对象com.javacode2018.lesson002.demo18.test3.User@1613674b
$ ]; H; `. }% S: `/ X( B 01d67154-95f6-44bb-93ab-05a34abdf51f' v! `: H% S2 d) y( a7 w6 f
第2次调用getUsername结束1 ^ W. {1 ]9 j4 R
********
4 v' m, d. d7 |9 c4 k ( [, g* _7 M# T, N, r( H
********; l1 M7 J) @! M. z; u
第3次开始调用getUsername
* v$ V2 Z4 p8 ^ BeanMyScope >>>>>>>>> get:scopedTarget.user, ^, U) ?7 a' T: G! F
---------创建User对象com.javacode2018.lesson002.demo18.test3.User@27ff5d157 t/ |, Y# p8 m$ b. e" g4 F8 n
76d0e86f-8331-4303-aac7-4acce0b258b81 E' v7 z. t; Z9 g
第3次调用getUsername结束. F1 C: J+ [% L. y+ V( G' P1 D
********; O8 \+ p3 U/ E, A" B. E
从输出的前2行可以看出:9 Y& N) t" `! G' K
* i$ {/ }$ M! C# n/ f 调用context.getBean(User.class)从容器中获取bean的时候,此时并没有调用User的构造函数去创建User对象7 c, H, ?+ i( x4 b
) ?5 ]% A) R# t! i0 z5 a. T 第二行输出的类型可以看出,getBean返回的user对象是一个cglib代理对象。 y: }! b, ]6 U; Y) {3 @, r' Z( q
& _, Q+ n& D- M0 p
后面的日志输出可以看出,每次调用user.getUsername方法的时候,内部自动调用了BeanMyScope#get 方法和 User的构造函数。
4 t) Q4 g& V+ D7 }! [; Q+ j( O 9 ?/ D) K0 f, U- a, s6 e
通过上面的案例可以看出,当自定义的Scope中proxyMode=ScopedProxyMode.TARGET_CLASS的时候,会给这个bean创建一个代理对象,调用代理对象的任何方法,都会调用这个自定义的作用域实现类(上面的BeanMyScope)中get方法来重新来获取这个bean对象。
y3 s; C# M* X6 I& V 3 Y) q# I2 k$ F: m
动态刷新@Value具体实现
, W) g4 B8 K% t4 g# N
9 M5 F% U4 c. K" l8 g9 f 那么我们可以利用上面讲解的这种特性来实现@Value的动态刷新,可以实现一个自定义的Scope,这个自定义的Scope支持@Value注解自动刷新,需要使用@Value注解自动刷新的类上面可以标注这个自定义的注解,当配置修改的时候,调用这些bean的任意方法的时候,就让spring重启初始化一下这个bean,这个思路就可以实现了,下面我们来写代码。
8 P A: @3 K& O( O) E
8 E A/ r7 @) g- r. Z4 s 先来自定义一个Scope:RefreshScope: M1 z9 S# S9 j. q3 W' Z
- j. k+ ?3 X8 z; t5 Q& b9 \
package com.javacode2018.lesson002.demo18.test4;" X) p0 w" d) j [ B: d, @" N6 g) h
$ P6 f% {5 a2 c0 T! D" E5 i
import org.springframework.context.annotation.Scope;
, w4 m& m8 X; n8 b9 d: w: f* s" z import org.springframework.context.annotation.ScopedProxyMode;
" G7 k8 ~2 K, `' T$ P/ X5 l 2 q/ N& {! q/ Z8 Y2 q1 ? U% L2 L
import java.lang.annotation.*;, h+ C; f; h7 \6 R4 ~+ l
3 j/ w0 ]+ e$ i- h- L6 Y+ H( H" n
@Target({ElementType.TYPE, ElementType.METHOD})
- \9 C' o) a- i( k0 W @Retention(RetentionPolicy.RUNTIME)
4 a0 [" M. P! ^, E @Scope(BeanRefreshScope.SCOPE_REFRESH); h. y+ M. J n4 s- i7 k1 W
@Documented
/ }1 x' f3 ]7 Y# o3 o, y4 b6 x public @interface RefreshScope {
/ ?6 Z0 v+ q: m4 R ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS; //@17 `1 F: D, c$ e" I* P
}9 D! ?/ s; J2 I7 t; k# j8 B5 [2 x
要求标注@RefreshScope注解的类支持动态刷新@Value的配置
! H# \/ F! f/ f9 `/ |9 z" {$ O ; m4 G. ?) @6 X4 S/ Z
@1:这个地方是个关键,使用的是ScopedProxyMode.TARGET_CLASS9 V3 Z! \0 n. H& i
9 S8 \$ z& z7 Q) o7 [( Q9 b" `
这个自定义Scope对应的解析类8 H; E# k+ \' j
# S. I" l) n% [& Q# X2 X/ F
下面类中有几个无关的方法去掉了,可以忽略8 b8 M% u- K5 z( R9 k' K
" x" Z' O7 a. D- d package com.javacode2018.lesson002.demo18.test4;" `& w* _, b! z/ A3 k& B
1 G( N0 A6 } f S2 G- E
: x/ s/ j u$ M+ X5 b% f
import org.springframework.beans.factory.ObjectFactory;7 F8 {, I0 x4 q p$ z! {
import org.springframework.beans.factory.config.Scope;
/ w8 k% U, a% x import org.springframework.lang.Nullable;$ ?) s! j- ?! l% L
( l |/ _2 ^5 C- K; P
import java.util.concurrent.ConcurrentHashMap;
, h0 Y+ |) t2 f/ M
0 y, i: h# P! Y public class BeanRefreshScope implements Scope {
- D2 s! K) g* Y& x& H ! k* s9 \! p" e" R
public static final String SCOPE_REFRESH = "refresh"; K+ i5 f4 {; V/ S- K5 f* a. y
4 M- p) z" ~' [) [ private static final BeanRefreshScope INSTANCE = new BeanRefreshScope();6 A; P( T7 Y1 L! ?9 ^! e7 D$ g! `
& \7 Q" o2 Q4 B- M7 W6 A' W$ z //来个map用来缓存bean9 g6 S# E0 A8 [5 K, f0 @: L) z
private ConcurrentHashMap<String, Object> beanMap = new ConcurrentHashMap<>(); //@13 j5 p$ @3 [6 F* \; z. v5 V
9 r9 f3 {1 e# e
private BeanRefreshScope() {
4 g* n9 }. i, x' V* h2 Q1 f6 C }% w" g! x+ W1 I, T
) y" c9 ]; o7 C5 @ public static BeanRefreshScope getInstance() {
9 q% S' m8 J2 B0 u return INSTANCE;
5 K4 E- ^7 [# r9 l! P s7 O6 g } s6 A4 U; H; }+ _ N7 ?' p
7 E5 x# M6 { j# Q1 d /**
, F% h* A) b' L: A5 {3 m. B5 G- S$ ^$ G * 清理当前& U7 L. t x2 H/ X( C7 Y! }
*/, R+ U8 ]0 v; C" @7 @
public static void clean() {3 t' {# o" D% W+ i- B
INSTANCE.beanMap.clear();
4 _, L) \* n) i) _) ~ }, J1 L9 u! v; T5 S* |: ~; }0 J8 m- t% O
& f. |1 X" v7 ]2 G+ e; t" ?
@Override0 W! p/ k' {# y% @
public Object get(String name, ObjectFactory<?> objectFactory) {
+ y5 h1 |$ O; n B3 ?( p4 W Object bean = beanMap.get(name);% [ C+ u2 P; k
if (bean == null) {
* ~% R' D0 W/ ~" J1 Q# I bean = objectFactory.getObject();. y1 s9 @% V" I; Q( X+ R
beanMap.put(name, bean);
& \1 r0 F5 e9 g# H* \. S6 |" R0 c' A }
7 k9 w: E% s5 X# T) @ return bean;' r- b8 ~. z. j( s" [6 c; C
}
( ~ G9 `/ S+ W3 l* l# B
2 a& `+ j5 ]/ [0 [ M2 | }- w3 k9 c$ r# @% A4 x
上面的get方法会先从beanMap中获取,获取不到会调用objectFactory的getObject让spring创建bean的实例,然后丢到beanMap中
' U* H9 c6 d- E: u" d, P8 W
/ ?$ @2 _. n9 _/ H. [8 { 上面的clean方法用来清理beanMap中当前已缓存的所有bean- q, [. C1 \9 u( Y
9 M* b" t! N! g; E0 Z/ V3 A
来个邮件配置类,使用@Value注解注入配置,这个bean作用域为自定义的@RefreshScope
, u2 D5 d7 |% x- k( n 6 P9 u9 i" [' h& m5 Z4 O- k
package com.javacode2018.lesson002.demo18.test4;4 K9 i, M/ h# _. t' j4 v
% A$ ^8 m8 \- X5 |( ]
import org.springframework.beans.factory.annotation.Value;: ^! ]1 H" i6 t
import org.springframework.stereotype.Component;
?" Q- h9 N; l' I6 a
! e2 e( ]' E w& Y /**
! J5 |5 F0 h0 Q" E * 邮件配置信息9 k% Q- `/ {. v/ {- |" P
*/
, R7 k- H% I0 N/ ]4 [: y& S+ } @Component5 W# D8 y$ y: k* f* @& [7 T0 |6 H
@RefreshScope //@1/ _! n2 ^ J5 e# ^
public class MailConfig {
' m! | |! L" M5 r: b/ C + m+ H- R" ^) w8 f* w( W( e
@Value("${mail.username}") //@2
6 x3 N( C$ s, L) E @+ { private String username;: {8 F9 T$ E; x; f' c
g# R# w! b7 [- y- \% y public String getUsername() {8 P0 z/ e$ J) B9 j8 K* W! z. X
return username;6 |- j7 c8 j! ]$ Q/ w) |& j
}8 n- }" \; y5 x8 }, n
' a' o* ~) u- d* s2 B s- J public void setUsername(String username) {
& K+ F9 t. l/ v/ \ this.username = username;5 _ {7 P2 B9 l: ]* M8 X5 |
}& S- `; n+ J% |- @4 X E% B) W
( k- y& ~9 J! j, ?8 ?5 @. a
@Override
+ X2 U$ I6 }( ~ w# ~ public String toString() {
6 @& j" c* W. d return "MailConfig{" +
" b+ p+ ?' i: a( z( f "username='" + username + '\'' +
9 z2 x) I9 M% F '}';" k0 n+ Y; h9 z0 Z. A
}2 f: {4 y4 g; D5 p6 k6 b: ^
}5 X2 a7 h( l' t' H) r; B
@1:使用了自定义的作用域@RefreshScope
! \+ `# w8 D* ]6 ^
u- H' I% r) O' O0 N& S5 ]6 M! Y4 b @2:通过@Value注入mail.username对一个的值
/ C4 O$ }$ X3 @2 e' a- h6 m
$ v s3 U3 U' F( t) `8 K O8 z 重写了toString方法,一会测试时候可以看效果。1 l( n* {5 V3 h! ] ?5 a' ]
* ~ @ B& ^( z7 R4 x% L
再来个普通的bean,内部会注入MailConfig
& u" M; H$ d$ b" O
3 c( L, L3 t! Q5 @% _( \ package com.javacode2018.lesson002.demo18.test4;
+ I) i, V9 ?, x* L; |, [
) Q# Z r% \- F/ u3 M import org.springframework.beans.factory.annotation.Autowired;
" L3 }4 N L3 h import org.springframework.stereotype.Component;
5 @& N0 n% L1 v! z0 G 0 p8 g' U# @7 G7 q1 {1 T1 ]
@Component
: f/ n7 b) j7 L' \, u( G public class MailService {8 o. L" n I$ g& X
@Autowired
6 N9 L" P. Q7 | w, I$ H private MailConfig mailConfig;
% M' k& m3 ?1 ~5 M& ?0 k! Z5 u + y7 e$ y) v7 h5 ?* T, K
@Override) i G' [% M' X6 h6 a9 \& l
public String toString() {
/ h' g) ]. K8 \9 L) o$ K return "MailService{" +( H5 U: n5 M, @! c9 r5 m
"mailConfig=" + mailConfig +
4 f! B2 U5 {8 _1 M# k9 M '}';
! v' d! N" Z# K }
5 y" t t9 Y; x& K5 g9 C }
" @) }3 p& b6 J. m6 D 代码比较简单,重写了toString方法,一会测试时候可以看效果。
. U: E& d3 N1 F" V2 s' N& t: M* _- V
3 h. D! s+ D1 b2 E' \5 v% j f I 来个类,用来从db中获取邮件配置信息: r# Y% P3 s# D1 {" W3 O8 ?
# b0 p' O/ t! W) Q" K. d$ o* M1 \
package com.javacode2018.lesson002.demo18.test4;7 c, I: M H. E& R1 z
8 k9 Y- O1 p' g! i, v* a$ o% C import java.util.HashMap;$ z& A1 w9 {: a
import java.util.Map;
7 Q% R$ n# i* n8 P8 u* b import java.util.UUID;
2 }" }$ W# o' D& N. q) K# q
* x2 v' P$ Z+ C, ~$ Z+ u2 @6 Z public class DbUtil {
$ @$ _7 Q$ K& C: } /**) G/ ?" D1 l6 X8 Q) J
* 模拟从db中获取邮件配置信息
- r- C d; M/ A& u5 s* \ *6 z8 D# x3 ~, b3 ]9 o5 r; y# p9 e
* @return9 c. b/ F/ n4 h3 }
*/
3 ^9 m6 {" N2 X public static Map<String, Object> getMailInfoFromDb() {
, ]2 V0 Q3 O2 t: x% N( h3 z6 i& y5 Z1 _ Map<String, Object> result = new HashMap<>();0 g9 _1 p- A% L3 d# }. E# a
result.put("mail.username", UUID.randomUUID().toString());! w, V; k/ o3 A, u* p' j3 _
return result;
5 [- \# f( R" g* k. p }
+ M/ v) n; v1 l; g1 S }
' a6 v# B! P7 C* E* E- `) n# N: b 来个spring配置类,扫描加载上面的组件$ V4 g& p( A3 J# }( i& V
# ?! K8 j1 h/ J f: M package com.javacode2018.lesson002.demo18.test4;
! d) k3 R' y* C
6 f' ?# N+ U# a% p' X- \ import org.springframework.context.annotation.ComponentScan;1 ~8 ~5 u/ G2 E x- n/ w9 j
import org.springframework.context.annotation.Configuration;2 W5 }# q' j: l, W% Y8 y8 ~5 `$ E. S
9 Q2 U$ Z; R" d) H) Q9 e @Configuration/ V+ ^6 U. ~; \2 @# y) h- C
@ComponentScan/ S: A: R$ I* F# p) W/ V
public class MainConfig4 {
3 S7 [. Z6 }( C; i& ?3 t. s, ? }' ^* o b* l3 t- N: v0 u* G- }
来个工具类7 U8 u6 t7 n, `
! O5 Y5 W# q8 L# D; J4 f
内部有2个方法,如下:
/ v( Y$ r5 J. ~ + E# n& [9 O* ?, U/ `$ B; ^
package com.javacode2018.lesson002.demo18.test4;; s& @# ^2 y6 w5 v: Q. |3 a
4 ^: g5 v$ r& a
import org.springframework.context.support.AbstractApplicationContext;% F% G* L \6 n$ e
import org.springframework.core.env.MapPropertySource;3 ?8 X2 R1 X; ]; v" l9 V+ J9 K
& y- w) z _) b- j" b1 L! K
import java.util.Map;
/ L; q9 Y% b4 `# B# {( S. P& A
* J# H) {2 r1 p. F1 ?6 j1 [: K public class RefreshConfigUtil {8 O, S! I# Y/ k" ]' h# C" H
/**# O& q; M/ `0 u, w# E: h
* 模拟改变数据库中都配置信息, P. P, f; [8 u& B
*/: t+ O* C8 Z" ^% c9 }' C2 U
public static void updateDbConfig(AbstractApplicationContext context) {
: D5 B1 f* S6 i //更新context中的mailPropertySource配置信息
- E& ?0 d4 f* d; x6 @, c. {+ b refreshMailPropertySource(context);8 P7 s" P2 \; @* u" M
9 Z: A! P; W9 ~4 ~1 P' I5 X7 `
//清空BeanRefreshScope中所有bean的缓存
3 g3 a" W+ ^, d& B. c- p BeanRefreshScope.getInstance().clean();- ^7 u$ y1 x$ [4 r1 y8 `
}
+ l$ X/ K, V! `% _1 q% I - d0 t7 }- ]" q% o+ m/ T& `& a* E
public static void refreshMailPropertySource(AbstractApplicationContext context) {8 P3 k( J* c* u
Map<String, Object> mailInfoFromDb = DbUtil.getMailInfoFromDb();
|) x- d- G, \5 M I: i //将其丢在MapPropertySource中(MapPropertySource类是spring提供的一个类,是PropertySource的子类)8 L( u* x( i* a# R) W* D. `% l }
MapPropertySource mailPropertySource = new MapPropertySource("mail", mailInfoFromDb);( C5 l5 R5 t- y5 U: Z
context.getEnvironment().getPropertySources().addFirst(mailPropertySource);# U8 `/ S k" s( @6 Z7 F
}: B: b5 a l+ b Q, B& u. g
" A+ [, d- h0 u0 `" J0 ^ }, Q) E6 U: T E+ Q0 y1 Q+ o
updateDbConfig方法模拟修改db中配置的时候需要调用的方法,方法中2行代码,第一行代码调用refreshMailPropertySource方法修改容器中邮件的配置信息1 c, c; c0 `/ ], o
: c8 L5 p8 w2 F BeanRefreshScope.getInstance().clean()用来清除BeanRefreshScope中所有已经缓存的bean,那么调用bean的任意方法的时候,会重新出发spring容器来创建bean,spring容器重新创建bean的时候,会重新解析@Value的信息,此时容器中的邮件配置信息是新的,所以@Value注入的信息也是新的。% ?5 M7 q" D% k: Q. V4 P" X( q
2 p$ Z4 ]8 `+ e" O
来个测试用例" F/ M. v$ {# v9 H+ X3 Z5 M
* }* T4 S% e* @! c @Test
& ^: L r' g6 W5 j% z$ ]* y0 N* Z public void test4() throws InterruptedException {5 @+ m7 t$ {0 ~$ Z0 n
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
- ~- ~6 \# x, Z context.getBeanFactory().registerScope(BeanRefreshScope.SCOPE_REFRESH, BeanRefreshScope.getInstance());
1 c0 s( @1 L& c$ F" |4 m9 L! y context.register(MainConfig4.class);/ W6 Q' @0 w3 W0 A, W
//刷新mail的配置到Environment
1 h4 N3 s$ A3 ?6 \ RefreshConfigUtil.refreshMailPropertySource(context);$ g8 @3 ~: @ K. m
context.refresh();
* F r" J% }, k/ M% W& j 3 E, q0 ~) E: {; z
MailService mailService = context.getBean(MailService.class);0 h2 _4 [( N9 r) X8 G: N [# G
System.out.println("配置未更新的情况下,输出3次");/ R- G! Z6 i: _; \# R
for (int i = 0; i < 3; i++) { //@1
* b/ i- E. ?6 l7 l) ]* b* y System.out.println(mailService); U, l8 b% w! r, c' T4 A) r8 a
TimeUnit.MILLISECONDS.sleep(200);8 N% Q( D# F0 m
}
3 P; @# Z6 i: [% x4 ]+ F 8 f' F5 U/ n3 ]% e( t6 H6 y
System.out.println("模拟3次更新配置效果");( s# G* S( j3 ?. O
for (int i = 0; i < 3; i++) { //@2
- W$ o# \' c+ t RefreshConfigUtil.updateDbConfig(context); //@32 Q; b7 p K0 { M. l# E
System.out.println(mailService);
: y' Q5 T% G* L TimeUnit.MILLISECONDS.sleep(200);
; ?: S3 Z" U3 @- H! g$ ?9 ?" `9 n }$ Q" o3 v/ n- _/ a, U
}
; v# h7 |# {- K# i, m @1:循环3次,输出mailService的信息
9 l/ e! T" a8 Y5 k
0 F& D, U! l7 [ Y6 {2 v1 o @2:循环3次,内部先通过@3来模拟更新db中配置信息,然后在输出mailService信息- U' B7 h2 U0 v" a: U5 i0 k! T
& t2 }- ]* G; a- f; Q0 ^9 D
见证奇迹的时刻,来看效果
" n: u1 B# z7 T0 |" K
9 |) q5 u8 l9 k$ B 配置未更新的情况下,输出3次
8 i0 T+ J( }* y- N MailService{mailConfig=MailConfig{username='df321543-8ca7-4563-993a-bd64cbf50d53'}}
/ j$ V0 a) Z( e1 E1 W6 N/ ?, { MailService{mailConfig=MailConfig{username='df321543-8ca7-4563-993a-bd64cbf50d53'}}
2 Z4 q& @9 M' v* q4 \ Z MailService{mailConfig=MailConfig{username='df321543-8ca7-4563-993a-bd64cbf50d53'}}
) u1 x/ p6 T' N& f6 G 模拟3次更新配置效果8 _6 }$ g1 \# F1 q( }
MailService{mailConfig=MailConfig{username='6bab8cea-9f4f-497d-a23a-92f15d0d6e34'}}6 s; s; B! a. p) D& b
MailService{mailConfig=MailConfig{username='581bf395-f6b8-4b87-84e6-83d3c7342ca2'}}+ e0 u3 z1 B( c7 Q4 R) t* n
MailService{mailConfig=MailConfig{username='db337f54-20b0-4726-9e55-328530af6999'}}+ |& ^) _+ N) T
上面MailService输出了6次,前3次username的值都是一样的,后面3次username的值不一样了,说明修改配置起效了。
5 {6 x. w2 s3 e; e8 |* M( [! F " F& f" V, |9 I9 D' {0 I4 B/ t: ?
小结/ K! H( s, @9 K) O
* U7 v5 v5 O0 W
动态@Value实现的关键是@Scope中proxyMode参数,值为ScopedProxyMode.DEFAULT,会生成一个代理,通过这个代理来实现@Value动态刷新的效果,这个地方是关键。7 B5 g) p4 W$ E1 B8 ?
/ W2 q. z. {' N* `4 l% M/ j 有兴趣的可以去看一下springboot中的@RefreshScope注解源码,和我们上面自定义的@RefreshScope类似,实现原理类似的。% I+ l1 i4 g7 \8 l
( Z* u5 q" j$ y( {
总结
4 H+ A0 _; ?1 a% g t: A9 g6 U+ m1 ], v7 N, h
本次面试过程中3个问题,我们都搞定了,希望你也已经掌握了,有问题的欢迎给我留言,交流!8 b/ |7 H5 M& L
+ G2 \. E% `9 y+ P
案例源码6 b: s5 K: H, D) Y
% d4 q$ e- ~( j0 ]: V, `9 ^! W https://gitee.com/javacode2018/spring-series5 Y+ v( L* ^. |& ~3 U
路人甲java所有案例代码以后都会放到这个上面,大家watch一下,可以持续关注动态。
. G# J1 J7 m' w# {+ I: N0 C9 s! W
& T7 E3 }! t- Q! @$ V Spring系列( @: ~ \: ~# U0 a
) g) w% F% Y' [/ b. I: O7 V
Spring系列第1篇:为何要学spring?8 r9 a" t6 K2 ]( h; E6 S" O+ o/ H
9 P' |# u$ }8 Z' |/ ^
Spring系列第2篇:控制反转(IoC)与依赖注入(DI)
$ F, D b" r. n/ ~$ K
: R$ \1 Q7 |# t5 o/ ?8 l1 u Spring系列第3篇:Spring容器基本使用及原理
" v x/ z' U$ s* C& S
8 Y" f8 E) E5 K5 A Spring系列第4篇:xml中bean定义详解(-)$ \2 u" l7 s% [* L* B* a& z0 L$ y
$ q. X* C5 b6 ~9 y: K8 l2 Z" e Spring系列第5篇:创建bean实例这些方式你们都知道?
3 F/ K! q' P( L$ V" F 4 g$ s. b) ~3 q9 B+ I: f" O8 E5 p3 l
Spring系列第6篇:玩转bean scope,避免跳坑里!0 ?; C- _8 Y( y; Y7 {
. V8 v( ~4 R+ N' G
Spring系列第7篇:依赖注入之手动注入
' b$ h* ~# H1 C, n; h8 v+ j 7 k# j- B0 Q4 x! X
Spring系列第8篇:自动注入(autowire)详解,高手在于坚持
$ _+ K: ~0 N0 v& {
$ K2 A; N* o5 P d9 t Spring系列第9篇:depend-on到底是干什么的?
. h" U' s4 L5 Y8 O9 [ " L& f9 u! I' w& ~+ Y
Spring系列第10篇:primary可以解决什么问题?
& i0 @2 H! `) V0 @( O" \$ ^
3 S2 M2 S: U2 ]" ^7 a Spring系列第11篇:bean中的autowire-candidate又是干什么的?$ Z7 `. T+ f3 j0 o6 V; H2 V
2 R' w. n, X6 A7 I
Spring系列第12篇:lazy-init:bean延迟初始化
. e* Z& r. o; u9 a9 K
1 W2 X; b- s2 k. ^* Y Spring系列第13篇:使用继承简化bean配置(abstract & parent)
z7 `% S; w3 j 1 ?' c( J# Z9 }0 ]2 f1 s* I
Spring系列第14篇:lookup-method和replaced-method比较陌生,怎么玩的?
$ y. J: R5 O9 u( w1 {& k/ Y. L* [ $ F' I8 a' m: ~: u
Spring系列第15篇:代理详解(Java动态代理&cglib代理)?
" `9 d: g* Q/ t
6 w; o6 t$ \& O. E2 t- ^$ j Spring系列第16篇:深入理解java注解及spring对注解的增强(预备知识)* e2 Z0 K! |* M' @* Y
+ Z: q. N }. Q' F5 u" o
Spring系列第17篇:@Configration和@Bean注解详解(bean批量注册)
; X7 A' q- T2 b4 d' g# Q+ @
6 n' u6 ~3 l0 U' h3 A Spring系列第18篇:@ComponentScan、@ComponentScans详解(bean批量注册)
) O, L- M1 S4 }, S3 x c" ~8 ]+ ]: Q1 F6 o1 m
Spring系列第18篇:@import详解(bean批量注册)
' Q. O& d# p2 a0 f. a & s, `+ m4 \( p, U5 h
Spring系列第20篇:@Conditional通过条件来控制bean的注册5 P5 C2 o1 g1 N6 M$ U
. {% }- M k, v3 d8 y& p. V2 ~ Spring系列第21篇:注解实现依赖注入(@Autowired、@Resource、@Primary、@Qulifier)3 f+ x8 {$ s" C9 ]+ q( [1 y$ _
" X" |' u4 i, g7 {+ r8 i Spring系列第22篇:@Scope、@DependsOn、@ImportResource、@Lazy 详解& h& V! v% _9 o# Y- s# W
5 m# r1 m6 Z' @ Spring系列第23篇:Bean生命周期详解
# \$ x: R# x& r% f! P " ^0 d# d D0 Z2 R! [0 S
Spring系列第24篇:父子容器详解' ~2 U% M$ b0 |. {7 B5 r. M; ?' |
# u& T8 N. {1 }. e5 _/ v2 N 更多好文章
+ L5 W, D5 T; F5 ?; _) @: X V+ j% l; _6 c$ t c5 Z
Java高并发系列(共34篇)
4 `' D' A! [+ [& u# i# E' @( _ 9 z; B& a+ a) k1 e, t
MySql高手系列(共27篇)
1 E1 ~7 U; _! X4 B1 W& L* a2 B: ]2 W
: o5 ~+ `& W+ B9 d Maven高手系列(共10篇)
- A/ n' y3 `% x2 G6 I( q
- n3 o) H- l: Z Mybatis系列(共12篇)
v* H! ~$ k. T5 x 1 f4 R0 Q8 v+ H" k d
聊聊db和缓存一致性常见的实现方式
- Q: _, C7 q4 q! I. I5 k
$ a5 b* G3 n6 d- _ 接口幂等性这么重要,它是什么?怎么实现?: E Q- D" x k- T- `3 M, u: z* ^/ R
/ d/ \$ y3 ]0 l 泛型,有点难度,会让很多人懵逼,那是因为你没有看这篇文章!' B. u/ a" _+ _* X$ _
————————————————8 n3 S( g: v; Z) Y, g W+ f
版权声明:本文为CSDN博主「路人甲Java」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。. Q# m" W% ]3 n3 M6 W; q
原文链接:https://blog.csdn.net/likun557/article/details/105648757
1 W2 f4 Y0 X- t6 m: q! b& P
: g4 R/ E* }+ g G% ` ( @! \+ Y7 h, a, w1 }
zan