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