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