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