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