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