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