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