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