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