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