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