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