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