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