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