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