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