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