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