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