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