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