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