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