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