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