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