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