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