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