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