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