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