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