数学建模社区-数学中国

标题: 太狠了,疫情期间面试,一个问题砍了我5000! [打印本页]

作者: 杨利霞    时间: 2020-5-23 11:01
标题: 太狠了,疫情期间面试,一个问题砍了我5000!
太狠了,疫情期间面试,一个问题砍了我5000!
  O, [* t3 K0 O疫情期间找工作确实有点难度,想拿到满意的薪资,确实要点实力啊!
( C7 l% W- B  Y) |
- \! @+ u5 Q9 ?: P7 P( n面试官:Spring中的@Value用过么,介绍一下& P1 A; t4 g; w7 z
8 |, C) Q. g9 q- G" U2 E) E
我:@Value可以标注在字段上面,可以将外部配置文件中的数据,比如可以将数据库的一些配置信息放在配置文件中,然后通过@Value的方式将其注入到bean的一些字段中
9 d) [) g$ m8 i( G4 q* H: t
0 j  m/ n8 j) f面试官:那就是说@Value的数据来源于配置文件了?4 I7 ]# F. x) p, S- k
8 N( e, I8 N8 C+ U! [) k8 ^
我:嗯,我们项目最常用更多就是通过@Value来引用Properties文件中的配置6 q0 C3 g; G  Y! E+ l' U/ }

5 Q( s- G4 W8 S  \. l1 m, l9 O面试官:@Value数据来源还有其他方式么?
8 Q' ?$ w! E8 k9 W3 L' v; t! h- _. P6 a* M( {" t! i
我:此时我异常开心,刚好问的我都研究过,我说:当然有,可以将配置信息放在db或者其他存储介质中,容器启动的时候,可以将这些信息加载到Environment中,@Value中应用的值最终是通过Environment来解析的,所以只需要扩展一下Environment就可以实现了。( a! d* @9 c2 \/ m* U  v1 F

6 s# x; S* o& ^# @+ V1 l面试官:不错嘛,看来你对spring研究的还是可以,是不是喜欢研究spring源码?
, ~/ Y& @5 M! T/ l! l( z3 f3 n! a3 I* N, I6 W9 ?' i, {
我:笑着说,嗯,平时有空的时候确实喜欢捣鼓捣鼓源码,感觉自己对spring了解的还可以,不能算精通,也算是半精通吧# I( L. n5 U: _- X4 v

; v$ O& ]$ o+ x) k* T9 D  ~面试官:看着我笑了笑,那@Value的注入的值可以动态刷新么?% r6 k! G1 }2 j0 {
5 T" h# L4 X. N  E3 z/ Z* }
我:应该可以吧,我记得springboot中有个@RefreshScope注解就可以实现你说的这个功能
; D, g! h/ O: ~
) ?" H7 h2 d$ z7 M* B; z# A( P面试官:那你可以说一下@RefreshScope是如何实现的么,可以大概介绍一下?
/ }" ~! |2 r  t* ]& V+ y' p1 [4 M- f' D  N7 V
我:嗯。。。这个之前看过一点,不过没有看懂
# v# c+ z0 `* y9 F' H% t2 z8 Y. w$ U- N) G0 T: c  j
面试官:没关系,你可以回去了再研究一下;你期望工资多少?
6 ~* n5 a' z. C4 p' [
; U9 y! |8 a; S" d我:3万吧& B" ^/ M! s" M1 e  m& e6 r
' a$ v; w% n' d( k  Y5 j6 L/ d  r
面试官:今天的面试还算是可以的,不过如果@RefreshScope能回答上来就更好了,这块是个加分项,不过也确实有点难度,2.5万如何?2 _  ^7 _6 C* @, z" Q4 ~# d7 V( o
6 c6 K5 b- ?2 R9 k! }+ q
我:(心中默默想了想:2.5万,就是一个问题没有回答好,砍了5000,有点狠啊,我要回去再研究研究,3万肯定是没问题的),我说:最低2.9万6 f4 Q. X9 r" `3 K. z" h

$ F$ K; f) _: y3 e# s  Y- g面试官:那谢谢你,今天面试就到这里,出门右拐,不送!) G: E. M7 h4 b/ L" P1 P- c) ]
8 `( X) h. O6 u
我有个好习惯,每次面试回去之后,都会进行复盘,把没有搞定的问题一定要想办法搞定,这样才不虚。! g3 |2 S& I1 K) n6 \

% S) b3 d, t/ e& s' W9 {3 h2 n+ ~% e这次面试问题如下
- z" B6 B" g$ [. d2 y  [& H) ?; A, U
@Value的用法
$ d) q) L! O" ~; c" H( k* _: }3 m
, v% d3 F9 }8 H5 j@Value数据来源6 _8 R; o5 k0 g# P; f* V' A2 A
. f& d1 R" A# N( e3 T3 q2 {
@Value动态刷新的问题
# m, R$ L. `+ A* n, l/ ^. ?% a# j  j5 W2 D1 Q% e: U1 O4 ?6 Z2 r
下面我们一个个来整理一下,将这几个问题搞定,助大家在疫情期间面试能够过关斩将,拿高薪。
9 |, r' H/ H7 [" ]* @
3 {. O/ i$ z/ y9 D1 ?5 E@Value的用法8 |* Z9 ]9 o) @

; ?: |) ~) N# ^  N% M* z6 L系统中需要连接db,连接db有很多配置信息。
" c8 t) @# W7 {& G. p' y( s$ Z3 K+ |
4 |$ y$ `6 @2 X/ x系统中需要发送邮件,发送邮件需要配置邮件服务器的信息。7 G# C  `+ G$ H

$ T% K. c  h1 j" Y还有其他的一些配置信息。) F  d3 o' K$ E$ }# c4 `3 [1 i0 p

5 S, ^2 t; n8 U# R. t我们可以将这些配置信息统一放在一个配置文件中,上线的时候由运维统一修改。! N; z: Y# d: T3 _7 U( d
+ W3 c5 R0 r+ a) g
那么系统中如何使用这些配置信息呢,spring中提供了@Value注解来解决这个问题。
# r/ ^1 j- N$ |' G) b1 Q2 U' h1 u) b* _
通常我们会将配置信息以key=value的形式存储在properties配置文件中。' Q. E' v3 i4 ?; H
! Y4 n' i1 b7 f; Q9 A& l' x
通过@Value("${配置文件中的key}")来引用指定的key对应的value。1 u. s. r, r2 u$ G5 S
6 b4 O) h# V1 R/ E  H
@Value使用步骤% E: z$ N9 D2 s+ Z
5 p* T( p, L+ E) Q+ g/ i3 J9 m/ j& X: i
步骤一:使用@PropertySource注解引入配置文件
" N' }; h0 l& ?+ p
4 @: |5 ]6 c' y8 U) O7 M6 K- g- M将@PropertySource放在类上面,如下
- B, Z" B% E3 v) h
4 q; W: @  F, J2 h4 j) m0 Q( }1 `' S@PropertySource({"配置文件路径1","配置文件路径2"...})$ {! z. n! {9 W: [) `
@PropertySource注解有个value属性,字符串数组类型,可以用来指定多个配置文件的路径。
* i! p' o% D6 P8 F' i+ P* I
" y$ I% j% ~  S  U% y* ^! R9 r如:
4 h1 l- N4 r$ h( V, Y7 B! Y: e' |3 L4 I9 @" i
@Component
6 D, H, q) ~/ E' l@PropertySource({"classpath:com/javacode2018/lesson002/demo18/db.properties"})
, S/ d( t2 c7 K  |public class DbConfig {/ V. X( k6 N/ X: a3 X2 C
}/ U  n& Z$ v. K- ^5 e% Q
步骤二:使用@Value注解引用配置文件的值7 g4 q: q$ k$ D) y

; x1 {9 q& [  r. l. J通过@Value引用上面配置文件中的值:6 G7 R& o9 ?1 w! G! k0 I

5 U$ X; m& o' A( h. C( w语法
. k( [" ^. O7 r# u; p0 z; c6 _# m1 T+ a
@Value("${配置文件中的key:默认值}")7 n. J! z8 W9 h+ Y0 f! N3 ]
@Value("${配置文件中的key}")
' l9 ]& \4 [9 t0 T, L如:8 u  u0 b; i" c3 a, T! |* K0 Q

  E* l6 _' }+ z1 Q/ N@Value("${password:123}")( f( N0 E$ b; H- T. S1 K* Q
上面如果password不存在,将123作为值" ~: V; i3 G& v; s# {: i7 y

: d- ?. z9 T& q, J@Value("${password}")3 J# d2 `9 u& p* B$ _
上面如果password不存在,值为${password}
  n; x4 L  Y. o" q) _* y1 u' S
" b5 m( O) O* e# k4 b3 C' C+ _# E假如配置文件如下
8 q, a' R7 M4 K6 j# e' L! ?, `4 v* I# p
jdbc.url=jdbc:mysql://localhost:3306/javacode2018?characterEncoding=UTF-8
" R8 H5 d1 G* @* zjdbc.username=javacode9 ?$ A. L- `! o$ ^4 T! \9 r
jdbc.password=javacode
" `  b' A! c. m2 R* o& O; x使用方式如下:
: x; N% ]% l& b  _- v5 K% ^2 w+ |/ d$ y6 e6 ^2 y
@Value("${jdbc.url}")
8 j4 I! N% y- n$ A7 o/ Hprivate String url;4 P- y6 o7 F! m; |. b2 M

  l) G1 g4 E  X3 |0 H  T; t( X- ]@Value("${jdbc.username}")
" H# x/ j6 t* Cprivate String username;: p5 W9 E3 G- p, C6 V0 Z9 t+ G6 `

2 M! @: V& E6 M; x. U@Value("${jdbc.password}")
( p; w, x0 Q) m+ ^private String password;
% P4 x* r4 d2 P& i6 e* m& g下面来看案例
2 A& V6 h4 B+ J2 p3 \3 Y
. u: m" Y0 f4 I0 T* L9 g案例
- e" p/ e2 k& w4 A1 `# O3 k+ M* s* V  ~* l6 C5 l
来个配置文件db.properties8 g! f$ E: D2 @& y1 {4 w/ u

6 s. K$ c* J3 ?+ _) kjdbc.url=jdbc:mysql://localhost:3306/javacode2018?characterEncoding=UTF-8
/ L  `" f$ ~" C3 O* L7 tjdbc.username=javacode& F+ l; k: j. ?# I$ M! i, y6 B
jdbc.password=javacode
" @2 f9 _* F1 r& l# G- ]& C来个配置类,使用@PropertySource引入上面的配置文件
9 ~- m8 ~( y+ y8 [0 N) n  ^8 X+ A7 }1 y: ?3 H+ R8 X
package com.javacode2018.lesson002.demo18.test1;
& l8 q1 D. a0 a! h8 H' {+ z/ w5 G* L
import org.springframework.beans.factory.annotation.Configurable;
5 @4 B* T# ^2 i: rimport org.springframework.context.annotation.ComponentScan;4 k) m, _- u8 x% F* k7 e# K
import org.springframework.context.annotation.PropertySource;1 I9 t! X1 I3 T! i" n

5 f& G3 O+ ^1 f: C@Configurable
! G) _- c& ]7 N@ComponentScan0 b# k! f8 a$ I. E
@PropertySource({"classpath:com/javacode2018/lesson002/demo18/db.properties"})
% S0 N  u, i2 t( [" J2 Dpublic class MainConfig1 {
) T- c. V2 X4 v& G. T9 l}; f- D2 b) [8 C3 L
来个类,使用@Value来使用配置文件中的信息
- p+ Y; D, f: n# `# U0 s! N2 q) Q6 Z
package com.javacode2018.lesson002.demo18.test1;
# Z  ^4 |8 K2 }; h! M" i4 h3 V  Y* ~
import org.springframework.beans.factory.annotation.Value;
, u- }( i4 R7 `& E5 f) p$ Rimport org.springframework.stereotype.Component;& I" K$ f: Z9 ?2 z# s( J

+ V% i- N5 G; x# W2 y& L7 B@Component
4 K2 W+ |/ e+ ^" ~2 A* w8 M) Y8 Ypublic class DbConfig {( A% |/ Z" V+ t! o$ k8 j

* H9 Z4 B1 d8 ~, p9 G    @Value("${jdbc.url}")% V8 L5 i3 G# L# h3 g
    private String url;: u# T7 w& L; B# Q3 o+ ^( F7 \& h
$ [# H4 _+ y& i% o2 ^
    @Value("${jdbc.username}")
4 F( o9 s/ A$ f& S# z) h2 \1 Y2 I    private String username;6 n" q( {% L; @: r' [$ \
% f; ^" u2 A* J# h. s: Y% G4 u
    @Value("${jdbc.password}")
* S: N) A: _" z$ Q( f5 h% E    private String password;1 g+ v  R0 `$ f0 S
& v5 S! n. Q3 z' y- l; O
    public String getUrl() {
+ V+ |8 t9 }: c0 N4 Y        return url;2 L/ v7 |$ ?! r) b! x- M
    }, G4 T. u5 V0 r5 T& U5 C5 i
; _. p, D' [, k9 y+ ~+ I
    public void setUrl(String url) {
, t* ^$ R5 w. s) F+ V        this.url = url;: S" Q' I; D  f0 I" G$ d
    }) O  N' P9 Q- ^" X( B+ i

; @9 e9 s5 Y- G    public String getUsername() {
( B- V- z3 k% d' S# l3 e8 I        return username;8 O# T! Y' ^( W+ i) a
    }$ M  I2 G1 U" m+ V( h/ r
  Y: p" L: p4 W) j4 {
    public void setUsername(String username) {
& F! z- j5 _2 f# J+ e- s        this.username = username;
2 h+ I  B8 F* S% e% i* q$ T7 V    }& Q5 u' |+ o  Y3 ^, y2 q
: p0 D5 J0 J% y+ D% O
    public String getPassword() {
- h7 y- y* m6 N- S7 o9 Z: M; O        return password;
; j! w5 z% F5 K/ b    }2 n, Z) m$ \8 r: u4 f# [) Q
9 u+ a7 S& N7 b6 h( n! R! J0 f
    public void setPassword(String password) {6 [' G! q# o- m& l- b4 T: l
        this.password = password;* I0 f  g7 O! a6 S7 e
    }0 m) }5 ^% o* G+ j! j% M* h/ n9 P+ k
/ W$ V/ K# n# R* _2 u+ H
    @Override
1 E% ?  a; R& |4 L3 k! K5 k2 B9 N' }    public String toString() {6 v& p. @( k% u7 t. C2 e: F
        return "DbConfig{" +
/ e* Z$ l( x7 m$ p( _0 Q                "url='" + url + '\'' +
7 T/ T4 m1 B2 ~6 k% {                ", username='" + username + '\'' +
0 I! R4 o2 y0 B                ", password='" + password + '\'' +9 w# l# _$ I. N- }( ~8 g' k  B
                '}';; t% s/ M) q5 d- P, c) Y
    }
# _& J6 [6 @& {" E5 W  q: S' [}
2 T2 i* V8 U' ~# ^& p上面重点在于注解@Value注解,注意@Value注解中的5 q# f2 @5 |6 F) \3 w4 w  A
  K% m9 l7 L7 c+ _! H
来个测试用例/ g9 B, [& J! e- ~6 e  f, z& e) b

7 p7 \. g# B( ~9 e: d2 x. Fpackage com.javacode2018.lesson002.demo18;
4 ~. l2 S3 X- g3 G
% ?8 p$ i& L# I1 d; V$ L+ }# F  c  G, {import com.javacode2018.lesson002.demo18.test1.DbConfig;
9 Z# A6 m2 B4 b6 j, z' yimport com.javacode2018.lesson002.demo18.test1.MainConfig1;
% G8 k% x+ j! F4 |6 p! m( Vimport org.junit.Test;5 |! s! |3 J9 w5 R9 h9 r1 u1 m
import org.springframework.context.annotation.AnnotationConfigApplicationContext;- S+ X; L1 P# U  R% w

* z! V: h& h/ Y, R' f, h$ x' ^/ Jpublic class ValueTest {
) `* L1 b- b4 @( f
5 [% p9 @+ S9 ~8 M1 g  E) Q1 g! Q    @Test! [8 j/ e6 s/ {: D) Y
    public void test1() {
+ n3 i7 J& y3 L- L        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();; p6 R2 T, [5 ^2 U% {8 i6 \, x# k, B
        context.register(MainConfig1.class);! G$ E2 C% a$ S+ z
        context.refresh();, Z! K0 E  c1 Q0 m" m0 l( H  c) K

8 Y) G( H3 }( o4 C/ `; _' o        DbConfig dbConfig = context.getBean(DbConfig.class);7 y% ~1 L4 l. ^- m
        System.out.println(dbConfig);
5 G, d9 x  u% u) g    }
# A3 p  c+ D. e}0 r8 n, K4 ]6 G0 J
运行输出; f% m, g( D  s' x
1 B8 Q) o( H# D: `
DbConfig{url='jdbc:mysql://localhost:3306/javacode2018?characterEncoding=UTF-8', username='javacode', password='javacode'}9 I+ H0 [* n& d, U% v6 s& J( w$ t
上面用起来比较简单,很多用过的人看一眼就懂了,这也是第一个问题,多数人都是ok的,下面来看@Value中数据来源除了配置文件的方式,是否还有其他方式。& x! s( C' B" ^  a6 t7 P

9 c3 \) F: A  K" F( z9 S@Value数据来源
/ F# _# o4 t6 V3 G' V, }- D1 G& z( N; `1 H. x4 l
通常情况下我们@Value的数据来源于配置文件,不过,还可以用其他方式,比如我们可以将配置文件的内容放在数据库,这样修改起来更容易一些。5 Z2 i: B0 P9 s  y5 H+ ?

" H9 C2 r" l1 O+ `( w我们需要先了解一下@Value中数据来源于spring的什么地方。+ D* j, Y6 G- V4 H% ^* @

8 S& g3 l2 @" }" ^' Yspring中有个类
: S/ ~; \7 ]# }0 K; c. O) @& ^* N* a- p# N. b9 s9 ?& a
org.springframework.core.env.PropertySource  U! R; {4 r5 M. O, l: L6 ?
可以将其理解为一个配置源,里面包含了key->value的配置信息,可以通过这个类中提供的方法获取key对应的value信息$ Z0 t4 H* R! Z  c
& @/ ^7 c( A) H, V" M& ]; v  `0 k4 u
内部有个方法:6 t. V& t4 I) R

# q. f' N8 M$ W' ^9 n5 O. o. Rpublic abstract Object getProperty(String name);9 \- a2 A, K4 J- [
通过name获取对应的配置信息。
1 C5 H+ a' F% N' ~! n4 F4 Y  r; g( @3 r# G, `2 x- I
系统有个比较重要的接口
- v8 N+ i& e) t2 G
( M- N6 y# G5 a" V5 h4 |: ?org.springframework.core.env.Environment
  C* b" ^* v. j3 h! e8 a用来表示环境配置信息,这个接口有几个方法比较重要
8 z% ?6 U& R$ N7 Z# j5 E* W9 d
8 N+ P: g/ @( R7 d, [3 r4 H6 h+ dString resolvePlaceholders(String text);
; I9 _! O: B, a: @. |; {+ [MutablePropertySources getPropertySources();
/ c& n; a5 U( n" `- ]8 GresolvePlaceholders用来解析${text}的,@Value注解最后就是调用这个方法来解析的。/ b& `) u( p. r9 C3 D% Z
" c* Z+ S) Q4 J- b, c  }, I% ]5 l
getPropertySources返回MutablePropertySources对象,来看一下这个类
3 n- |; n( k  E7 d
2 {2 [, W5 f0 [5 @7 e! l. d" w2 Epublic class MutablePropertySources implements PropertySources {
6 g+ K0 w% ^: u( {( e2 y9 C! }) [" l0 M3 w3 V
    private final List<PropertySource<?>> propertySourceList = new CopyOnWriteArrayList<>();6 Y8 W- C- L7 z9 y5 G6 Y
* i3 N7 ~; d# ]  h% ~( X% C
}
/ O1 D5 M6 [4 n( U4 ~内部包含一个propertySourceList列表。4 R# S' d3 g" t- y( |

. v# m1 d* ]+ D( `) E& zspring容器中会有一个Environment对象,最后会调用这个对象的resolvePlaceholders方法解析@Value。% P" L7 W1 p4 ]& }' W
7 k! \, M' R/ r+ I  _* a
大家可以捋一下,最终解析@Value的过程:
  p  k) V% R( v1 G" s0 y9 k6 j% l3 e* H$ ?: R
1. 将@Value注解的value参数值作为Environment.resolvePlaceholders方法参数进行解析
/ a. T% ^8 Q) t& L+ H2. Environment内部会访问MutablePropertySources来解析9 }1 }+ A$ o" c6 C" e. H5 i
3. MutablePropertySources内部有多个PropertySource,此时会遍历PropertySource列表,调用PropertySource.getProperty方法来解析key对应的值; H3 i; d  t8 y+ l3 s# l
通过上面过程,如果我们想改变@Value数据的来源,只需要将配置信息包装为PropertySource对象,丢到Environment中的MutablePropertySources内部就可以了。8 t# |( u- G+ \1 y; a/ A0 j" R' s  Y

7 v1 x4 ?9 l# k. b下面我们就按照这个思路来一个。2 g4 g( W1 D2 A- P+ M+ K$ f* m) f

* P! @# p9 o; Q" D# ~, N来个邮件配置信息类,内部使用@Value注入邮件配置信息
+ R9 K: [: }+ C* u* C
7 ?$ g1 }! X, c6 j0 mpackage com.javacode2018.lesson002.demo18.test2;$ b: A( ^0 N2 I$ X2 J7 E

: j6 J: u( ~& K# T) A! }import org.springframework.beans.factory.annotation.Value;
+ R7 ^3 |8 H3 A2 U  Pimport org.springframework.stereotype.Component;
; }  |2 w: g- A: T
( F+ w! h( p9 q6 E7 C0 Y, K, C/**) m2 D; l7 F( O% O; q$ z: u( Z4 Q
* 邮件配置信息5 J0 u' S; E& n" D: C
*/
7 f6 B6 y, M( ]9 q@Component
1 }. x9 J4 A. m* q* `" Rpublic class MailConfig {
% E$ q* `. }3 a3 }6 j: i, ]6 T0 F3 e" [5 v
    @Value("${mail.host}")
' t2 }! D2 _. p+ N* D2 L  w    private String host;  D- P7 O) }* q5 N

' A; X- M; B% s5 ?2 s4 h    @Value("${mail.username}")" K* R+ J6 l( T4 I+ U" j6 [' g/ u
    private String username;
7 E) h' n! S& y0 Z2 ]) {- l# O' Q. D" ]7 J$ h* Z4 m
    @Value("${mail.password}"). {! b& e5 ]$ L6 U: s* d1 {4 I% E
    private String password;
; f4 ?' V8 }  v- ]4 X; d1 @% I* _" |; |" f
    public String getHost() {9 ^2 \8 ?5 V* C4 v5 P/ d
        return host;
' d" b7 \3 V: k/ V: ]    }
( Y) H, f# P! U9 o7 A9 L
& s* |! P9 r1 I. m- l% k1 p    public void setHost(String host) {8 p- Y8 @4 J2 E* C4 v
        this.host = host;
) L5 O4 p; N2 g- X# Z  ~    }: }% n7 t8 R7 F$ P9 J6 d4 g
7 w" G2 T" g' G# `- A
    public String getUsername() {# b+ b, v$ q; Q; ^
        return username;
& s9 Y& D3 v: m7 L( C( s8 g% U& W    }
, \$ D$ N8 O5 B' t4 n* }3 @+ b6 K) U3 ^- i# E2 V3 d
    public void setUsername(String username) {
5 }# `7 k7 j# x$ Q        this.username = username;
0 Z6 O1 A) N: P7 `3 M6 `4 `( H6 C0 ~    }
$ i- \5 w( n* D3 \/ T) Y- i! g) x5 K
    public String getPassword() {+ u: G, Q. _3 ~! p# ]& N" l
        return password;, a# ]1 J0 R8 @/ p" b5 G, O
    }6 B) C& S6 h; x0 e
0 N1 k+ r' O: H; U* N+ N: t
    public void setPassword(String password) {
  F& Y% \" p6 D8 g  u        this.password = password;
) w  d- x; v2 T1 {* i% ]7 m    }
4 K6 D  g' a4 [/ m: Q  [. F1 W& x8 Q$ F& v( A# ^% Z( u8 I# _
    @Override$ B/ ]/ J4 w" n3 d* a
    public String toString() {
! x& q* j1 E7 e5 R/ h        return "MailConfig{" +% f8 @4 S" j" J3 l* S) o8 z' j/ |' @
                "host='" + host + '\'' ++ K  o% h2 X8 l/ M  [- `, B$ ^
                ", username='" + username + '\'' +6 N" ^% `- v) D2 T2 y6 i
                ", password='" + password + '\'' +
' w# H4 `& X2 l5 w                '}';
, k3 n, Y5 b* r1 G, {    }
$ ~" o9 x0 h$ S+ c}3 E/ W6 ~# Z' c, R# ]
再来个类DbUtil,getMailInfoFromDb方法模拟从db中获取邮件配置信息,存放在map中4 U( V* F' i& G
) S) @+ y; @& |7 f" e2 i, p
package com.javacode2018.lesson002.demo18.test2;9 p. E  g  P1 S9 w

) d) C, J# b+ kimport java.util.HashMap;7 {% m+ J7 D/ s7 f
import java.util.Map;
' T/ D4 u/ E7 \8 u
$ J; J6 U" H, v) tpublic class DbUtil {* v& D" U$ y% Q6 A: R; A0 \& t7 A
    /**$ i) c: E. R. M1 `2 r$ v1 \/ _3 X
     * 模拟从db中获取邮件配置信息
% ]; d/ x1 J6 {8 \+ R4 ?" [     *
- y2 u' X/ c6 G* t, `+ }8 S5 J' L     * @return' i! ]8 }9 x/ X" m/ Y$ L( M
     */
/ t; B2 B+ K  |; D' |    public static Map<String, Object> getMailInfoFromDb() {! t2 g; w$ T- [/ J' j  t4 W
        Map<String, Object> result = new HashMap<>();
$ x: |( t# L1 w9 N        result.put("mail.host", "smtp.qq.com");
' l. l8 l& X$ Y- {& ?( w; Z5 b        result.put("mail.username", "路人");
( M6 A; X# A4 e6 j5 V9 T( Z        result.put("mail.password", "123");
7 ]0 O3 y3 B7 }! p' H        return result;
! Q. r) `- W& G/ w* c  [    }- Q7 W: l4 {4 u
}
; \% C2 z& a" v% Y( Q5 D" M来个spring配置类+ \: h# F! q% d) S6 j2 n  f

  k" J! i9 f- o  ^9 Jpackage com.javacode2018.lesson002.demo18.test2;
5 Z/ i4 S3 S) b4 I! Q
- u' W  r1 L+ _) e* Q7 G$ ^import org.springframework.context.annotation.ComponentScan;# ]1 W1 R7 V) \, J( `
import org.springframework.context.annotation.Configuration;: ^/ }& M2 Y3 {( v* K
2 l* J* \1 J# z# K4 }
@Configuration, E$ H( u8 H0 f7 ]- _- i
@ComponentScan7 ]% @7 O$ ~7 `
public class MainConfig2 {
. G4 J/ c4 m& [. e7 u- G}
4 g% X' t4 |. h! B2 i% |下面是重点代码1 h0 q/ \+ y9 j$ c7 {8 W; }% W$ ^# l
3 v9 p( l  V2 m, A
@Test
- Y5 o3 Q/ F& g' ]& I, L* A. ppublic void test2() {
; K4 l5 A- J0 V- l    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
! I* X4 H' G$ Q7 p* e  M
; \3 j2 Q0 x/ ^    /*下面这段是关键 start*/# n; X: Y1 D/ \. R' l3 g( R
    //模拟从db中获取配置信息7 i7 K& q) U; M8 z) W
    Map<String, Object> mailInfoFromDb = DbUtil.getMailInfoFromDb();0 V; D4 q8 L3 n  F) }
    //将其丢在MapPropertySource中(MapPropertySource类是spring提供的一个类,是PropertySource的子类)
6 _* l1 M8 ]8 A5 W, l1 v    MapPropertySource mailPropertySource = new MapPropertySource("mail", mailInfoFromDb);7 t% Y4 H+ ]; L1 t0 X
    //将mailPropertySource丢在Environment中的PropertySource列表的第一个中,让优先级最高: N" h$ j" ~6 T- y: I
    context.getEnvironment().getPropertySources().addFirst(mailPropertySource);
" Y7 [3 |; {# N1 @$ l' c# Y! M0 \    /*上面这段是关键 end*/
( N+ }" ^! M3 d! {; v
8 d/ R1 t/ {" ?( j/ N" ~5 X, B    context.register(MainConfig2.class);9 Q+ t# A( X& J4 p8 y( i
    context.refresh();3 Y& @/ U% y% ^4 [
    MailConfig mailConfig = context.getBean(MailConfig.class);
3 v8 s6 X3 [4 T. N    System.out.println(mailConfig);9 a0 n9 e6 c! B) V. Q9 \4 O. `8 k: Y6 v
}, b- _  V9 E# u- u1 @8 O
注释比较详细,就不详细解释了。
  e* v2 O' q% E4 Z' U3 w
; J5 F- u  e( C4 G8 Z直接运行,看效果
# y% _+ n3 e5 |& a8 D/ Z
9 c6 s& ~" I9 C$ N6 _MailConfig{host='smtp.qq.com', username='路人', password='123'}: [7 o3 F8 \6 L7 @8 t& _6 f5 \
有没有感觉很爽,此时你们可以随意修改DbUtil.getMailInfoFromDb,具体数据是从db中来,来时从redis或者其他介质中来,任由大家发挥。1 W$ r# A) N' e6 d" t
5 l- q* A4 f; ^8 D& v
上面重点是下面这段代码,大家需要理解. ^, g8 B5 t3 s6 Y! z/ o+ P

& g4 h! m3 ^; B) `7 D* o8 ?, n1 g8 J! ^/*下面这段是关键 start*/
( C: B8 u5 t; X" j" D1 o//模拟从db中获取配置信息( q% x/ r& c# m& [
Map<String, Object> mailInfoFromDb = DbUtil.getMailInfoFromDb();( _, H" B% ~3 j
//将其丢在MapPropertySource中(MapPropertySource类是spring提供的一个类,是PropertySource的子类)
0 S( M( D% R& \( f$ OMapPropertySource mailPropertySource = new MapPropertySource("mail", mailInfoFromDb);2 c3 Z; g5 \# j" X
//将mailPropertySource丢在Environment中的PropertySource列表的第一个中,让优先级最高
/ E4 I* N8 H. ^" M+ ~context.getEnvironment().getPropertySources().addFirst(mailPropertySource);
4 n% L& {" v+ B4 r3 \/*上面这段是关键 end*/& Q8 _5 d% j, j1 M. L1 }  P$ x
咱们继续看下一个问题
& H0 D: p$ k0 ^3 S  D2 ?1 e
5 v: w2 J/ F+ M4 C: y. ~& F+ s! I% ?如果我们将配置信息放在db中,可能我们会通过一个界面来修改这些配置信息,然后保存之后,希望系统在不重启的情况下,让这些值在spring容器中立即生效。
+ D* Z* S+ O0 U2 h. H; n! I5 Z/ n3 C+ v" b+ `' m1 n5 y
@Value动态刷新的问题的问题,springboot中使用@RefreshScope实现了。
9 ^- z9 @: }* O' M
  q/ x) s0 M, \; J7 \实现@Value动态刷新
3 v! [7 z& M- q0 l( c) x. t# K/ q4 D
先了解一个知识点$ Q+ w7 i+ {" _% v( t. p
% [. f$ u+ X0 ^, m# u
这块需要先讲一个知识点,用到的不是太多,所以很多人估计不太了解,但是非常重要的一个点,我们来看一下。, Z. b4 Q8 P/ |  x) q7 r/ e
$ P6 a1 J/ Y- E! M; {8 {
这个知识点是自定义bean作用域,对这块不了解的先看一下这篇文章:bean作用域详解, q" @0 ?! o6 w- C9 \" `- J
2 @( ~3 E( a( \) R$ \
bean作用域中有个地方没有讲,来看一下@Scope这个注解的源码,有个参数是:$ R( H4 U# `# S4 D* |5 I
0 [& `. O8 b( w9 Z
ScopedProxyMode proxyMode() default ScopedProxyMode.DEFAULT;
7 x  f2 W- d+ e' |: |; @" w这个参数的值是个ScopedProxyMode类型的枚举,值有下面4中
1 ?# n: E& I2 B& @7 p" a7 {6 @7 x1 d  h
public enum ScopedProxyMode {
* u5 c" `# c! d1 t# p  H& `    DEFAULT,
. X! y. j) N6 |- a9 j% y4 ^    NO,: t7 P( T6 p, D" N2 [( c# [; u  v
    INTERFACES,% ?8 v% ^$ o# g
    TARGET_CLASS;% d9 ~: q8 O- u: t9 U5 U" w
}% R: W5 X( Q9 v, G+ y
前面3个,不讲了,直接讲最后一个值是干什么的。
0 ~8 U" [8 V- }0 H
# V; S# |% W2 H- N, Q- C+ j当@Scope中proxyMode为TARGET_CLASS的时候,会给当前创建的bean通过cglib生成一个代理对象,通过这个代理对象来访问目标bean对象。
4 \# ^  V! u9 o: R0 I. u& G) T
理解起来比较晦涩,还是来看代码吧,容易理解一些,来个自定义的Scope案例。
+ R* w2 r1 a* ?3 w5 a" R9 T4 l% |- C! J! H* `) x1 R# l& @
自定义一个bean作用域的注解
$ v6 b- K* e7 y" a& H8 J0 T8 K: S7 N9 ]- j0 E1 Z% L/ o+ N$ y& B
package com.javacode2018.lesson002.demo18.test3;
: i: ~* }# v) a5 e" R7 y
6 N& X6 @4 j0 v2 {import org.springframework.context.annotation.Scope;
6 s# E0 B/ c; S8 y/ P# ?5 Gimport org.springframework.context.annotation.ScopedProxyMode;2 k0 U! B' U# ^

, o$ K! S8 c0 b' `: L8 nimport java.lang.annotation.*;5 B  P; o* e% T

$ ]4 c, p+ x8 n+ N- N@Target({ElementType.TYPE, ElementType.METHOD})
1 r  ]) R: w6 W( B@Retention(RetentionPolicy.RUNTIME)% w3 a: \3 L1 j/ D' r5 V/ }! j
@Documented
9 p  V; e9 l# s# {@Scope(BeanMyScope.SCOPE_MY) //@1
1 _+ V2 v% }. l% W5 M4 H9 Gpublic @interface MyScope {" B- ^" N  p) W/ M& L" Y6 p' B% \2 M
    /**
8 E6 m; w1 d+ G* E- C4 {     * @see Scope#proxyMode()
7 N- r! i/ Z+ @3 e" U! z9 ^     */
( K* C1 F6 `8 G! ]6 U( i7 w8 D    ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;//@2: }  z% y) ~3 J& F0 q% y9 p" K% S
}
' {' b% G/ z# X# e' z' B; p@1:使用了@Scope注解,value为引用了一个常量,值为my,一会下面可以看到。' e( u: ?( o  g9 k3 {$ w& A
% p+ w( r/ v, f: x6 L  O
@2:注意这个地方,参数名称也是proxyMode,类型也是ScopedProxyMode,而@Scope注解中有个和这个同样类型的参数,spring容器解析的时候,会将这个参数的值赋给@MyScope注解上面的@Scope注解的proxyMode参数,所以此处我们设置proxyMode值,最后的效果就是直接改变了@Scope中proxyMode参数的值。此处默认值取的是ScopedProxyMode.TARGET_CLASS
' \) A- H* h- e- u; ]* e0 }
0 [# C  ^) r: U2 d@MyScope注解对应的Scope实现如下
! I9 l8 k4 A. P& ~6 @
1 k1 U  ?. L3 Q, spackage com.javacode2018.lesson002.demo18.test3;5 S) |, W' A0 A# I! \+ `. s' Z7 Y

0 y' N  I7 p9 Y' o1 R+ |5 Himport org.springframework.beans.factory.ObjectFactory;
, _+ w! a  P9 v/ nimport org.springframework.beans.factory.config.Scope;* N& p4 o; I" t- Z# Q
import org.springframework.lang.Nullable;! U% b3 ^/ X+ [" n4 ?  w

+ m" E% F5 o$ A/ ^% g* ^; Z- k/**2 [/ r' U4 e3 P7 C
* @see MyScope 作用域的实现5 [4 }* A4 @* @, P+ e! A
*/7 y8 `8 M+ K8 q- V! X& G
public class BeanMyScope implements Scope {
7 O8 n& r/ F  n- I6 b; C" c& s; l3 M' V. \
    public static final String SCOPE_MY = "my"; //@1! D+ o' J- I- ~

* [0 q% `7 j2 r" R    @Override
+ [! V) d! F$ i. w9 P( k    public Object get(String name, ObjectFactory<?> objectFactory) {
) c* o4 b0 ]. t& k4 v4 Z        System.out.println("BeanMyScope >>>>>>>>> get:" + name); //@2
/ Q/ `) Q. @( U8 p  n        return objectFactory.getObject(); //@32 s1 u* m9 I/ W/ _- J  g/ n
    }; M1 U: n2 f5 B, v8 n. I

  p9 `" `7 \$ R( J  y/ w    @Nullable" J" y( X. D  K& q1 \/ C% _8 j
    @Override
6 w# P/ q( q) e! M" m% U    public Object remove(String name) {
. U+ Q: i) e  b) {; g8 F1 }, ^        return null;
6 I" Z4 r# F) q3 e    }
% N8 F. Z& \3 i3 N% V
3 `# m# }' |$ k+ R$ k, Y/ ]    @Override- z5 n$ \; n9 o+ P, B
    public void registerDestructionCallback(String name, Runnable callback) {3 I9 O8 e& W0 V! M

4 i( x9 L9 @; \  }. e    }
5 g" p7 S' \( Q( ]5 o( n  [+ V0 @" H
    @Nullable
+ @( r' p- n6 x% x: a    @Override, t& S( Z$ K( f6 f4 J
    public Object resolveContextualObject(String key) {
, g' ]* P$ r; I; X1 ^8 R+ B0 J0 f        return null;1 y! C& D; H$ Q. u
    }- A/ K0 C- q$ m% G, ~5 }- V' M

+ Q, p( L) {  j3 ?    @Nullable
3 F/ I1 Y& H/ H  b, F    @Override( `! e# q6 `5 i* k" Z9 |* Q
    public String getConversationId() {6 w, u9 u* u3 P' P' _
        return null;
* C4 O) i+ v% k    }
4 Q2 [8 `  B; `9 }  i) z; f}; p% _9 r( c) K! K( e* k6 B
@1:定义了一个常量,作为作用域的值
4 C8 }8 M. [# s$ b5 j8 Q0 e& ~
( e, c% U& z% I) k7 }# w@2:这个get方法是关键,自定义作用域会自动调用这个get方法来创建bean对象,这个地方输出了一行日志,为了一会方便看效果' Z3 y2 `/ C( z9 e% _  d
) v# X1 g$ h+ v
@3:通过objectFactory.getObject()获取bean实例返回。
% e: z+ E! U7 Z0 ?0 Q9 i+ K
  C. S" v3 [. `2 f下面来创建个类,作用域为上面自定义的作用域
6 ]- {7 ~  z- Q5 e1 O5 Q4 x- m+ J4 Z
package com.javacode2018.lesson002.demo18.test3;
, ]% J" \% h! ~
! k% d. C8 @; i% p% b( \3 v) Uimport org.springframework.stereotype.Component;
9 B2 z8 D: t, M( e3 n) U4 n
5 G  q! p" C- @# V! _- v) {% y' C/ uimport java.util.UUID;9 Q1 M  U  H4 C4 m, U" v6 a" V7 E/ d

: `6 j! [) k" g( X7 D@Component
5 L" Q& `/ s0 K& d  X3 i# v- g+ L+ z@MyScope //@1 ) d$ Q! d5 P) b
public class User {
( j6 o# h; ]2 W! G8 G  ~( r* [. W/ A9 n& {
    private String username;0 j8 o: S5 Y* r# ]: `7 R

& u/ {3 l% X' T* ^2 j9 L    public User() {
& R0 c; B2 z0 V        System.out.println("---------创建User对象" + this); //@2; g$ d) q! f0 y8 b+ p! h
        this.username = UUID.randomUUID().toString(); //@32 s2 ]$ y( E: @; Z) S" G+ [) H0 m
    }
2 T( K4 F, G* k$ O2 t5 ]1 g  s, f( @. I' L
    public String getUsername() {/ T6 v# i- }1 Z9 R) E. U
        return username;
7 o$ U8 \0 k* T) Y1 A; s& N    }
- d9 I+ O6 B0 Z/ ]  O7 _9 S8 u8 l0 n! P" L8 N3 o; s
    public void setUsername(String username) {
* l2 c; F( L) y, @        this.username = username;
# \& q+ i. \$ K2 s% ?" p0 r    }
$ D3 e/ Y. J3 g- @& e; a
9 `8 [$ z  v( v) L+ O}2 M8 s/ {: K, s# M
@1:使用了自定义的作用域@MyScope
; Y! u! l0 r/ I
% r- K. k) G4 t: V" c8 S6 A& E: W7 C@2:构造函数中输出一行日志
4 e  R" i- @6 [# i
& K4 R! H+ C' Z) U4 }4 X@3:给username赋值,通过uuid随机生成了一个
% L2 A& v- l; y8 C9 X; n+ e1 P( C- v2 j& }! ^. K
来个spring配置类,加载上面@Compontent标注的组件+ m4 z5 K5 R, o5 T2 D& m2 ?

5 f1 R0 z% a% a% hpackage com.javacode2018.lesson002.demo18.test3;3 Q$ a! x0 h/ ~0 L5 L, a" D

) C( R4 v" b9 S9 z& Mimport org.springframework.context.annotation.ComponentScan;6 R( q4 K' D( i/ n5 V# K; m. W4 d
import org.springframework.context.annotation.Configuration;8 l: f3 _, X9 b2 w' l" s( F& U

' }- ?$ E  v. Q. H@ComponentScan
5 X) C5 e- b% U: T@Configuration1 `2 f: y; E5 D% s% q7 A
public class MainConfig3 {
' F6 I8 \! p0 L# a& l* }}6 i9 p! D; q$ f8 V) w$ _7 Y! Y
下面重点来了,测试用例1 e3 y- X0 @, X! V9 l

* [; K6 U1 w( K  w@Test
8 x/ h  a& i5 g/ o2 X2 K( I5 Dpublic void test3() throws InterruptedException {
" n& q9 {4 ?6 e  d- I3 I, K, c    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();0 x) F  j! p3 E( Y4 z; q: r6 l4 J
    //将自定义作用域注册到spring容器中
3 F5 X' i7 E3 G9 x+ P$ F    context.getBeanFactory().registerScope(BeanMyScope.SCOPE_MY, new BeanMyScope());//@1
6 [5 B& ^, }- y+ B* c9 q2 L    context.register(MainConfig3.class);
( A2 }, {. E3 b' x2 `    context.refresh();
9 @& l- }" _  I/ D& J, n/ j7 B4 k2 y7 T
    System.out.println("从容器中获取User对象");" d/ b- `6 n2 R: g) m3 b
    User user = context.getBean(User.class); //@2
" u5 n9 N! p/ L1 p/ L    System.out.println("user对象的class为:" + user.getClass()); //@3
, R$ P1 {8 T) s2 S0 `' G7 ~$ R. M0 n: T' q* d  P8 i
    System.out.println("多次调用user的getUsername感受一下效果\n");
) B8 F9 J) n! X7 W* Q7 c! ~7 n    for (int i = 1; i <= 3; i++) {7 k) {9 {# t* d" ^- ^( v  e6 K
        System.out.println(String.format("********\n第%d次开始调用getUsername", i));: l/ _: `/ U5 A
        System.out.println(user.getUsername());9 l8 J2 p+ y1 h* s* e4 e6 @  G( a
        System.out.println(String.format("第%d次调用getUsername结束\n********\n", i));4 i1 p+ p9 x3 A: X+ Z  F; ~3 q
    }
% b+ o9 J/ Y% z/ t) Q}( o+ D7 q" y) @5 q- u1 M
@1:将自定义作用域注册到spring容器中
& w- l# Y. E0 A4 k. ^( [9 ?
- _; R* b9 T* x" b; S, G@2:从容器中获取User对应的bean
" x9 z3 ?  o2 H( n# U# b+ f5 a! Z4 y
@3:输出这个bean对应的class,一会认真看一下,这个类型是不是User类型的! R2 h( a2 \* ^0 t; _

& X( R' I" c4 W/ T  R- c代码后面又搞了3次循环,调用user的getUsername方法,并且方法前后分别输出了一行日志。
6 p- H' k1 O& p0 Q( S! G8 R$ v& l* |6 c
见证奇迹的时候到了,运行输出
, ]* `. |/ Y/ b, v5 w  b8 Y$ }" F; c: ^, _4 i
从容器中获取User对象
% r  T. x( l- N% X; D* cuser对象的class为:class com.javacode2018.lesson002.demo18.test3.User$$EnhancerBySpringCGLIB$$80233127
( Q5 R& n' R$ [& b$ s9 l( c- l1 J多次调用user的getUsername感受一下效果- N+ R* `3 v8 d' B) M' `6 q
$ n/ K3 p. ^* ]2 I$ }% r
********
" p& |' O% C" |第1次开始调用getUsername
, Y" J3 P) R8 f$ V4 b  ]BeanMyScope >>>>>>>>> get:scopedTarget.user
+ |0 {6 v/ p3 j* w---------创建User对象com.javacode2018.lesson002.demo18.test3.User@6a370f47 F& h5 k9 m9 d3 m- c3 G
7b41aa80-7569-4072-9d40-ec9bfb92f438- q' f( x" M; A& |
第1次调用getUsername结束
4 w! y/ d3 p$ _0 s********( d- ?* g2 v' j7 C! U
/ ~- I( F7 M% f; z5 U1 ^! Y% z
********
! e0 `: P, r/ m+ c* O% I第2次开始调用getUsername# |, j5 R3 ?. O: p+ [4 j& q
BeanMyScope >>>>>>>>> get:scopedTarget.user* H/ ~5 }$ G4 |9 [7 P, E; s+ C9 ^5 K
---------创建User对象com.javacode2018.lesson002.demo18.test3.User@1613674b$ V1 w  q* y+ A$ \1 S& b
01d67154-95f6-44bb-93ab-05a34abdf51f
# y$ }, H" k6 `5 a& x. d. ^1 T第2次调用getUsername结束* T" h, X+ d2 n$ b5 W  j% _1 b7 L
********
, d5 a1 f6 Q' R) T4 ~  W& w+ J; }; k$ ?
********
5 G( z1 B2 f6 R3 ~+ f第3次开始调用getUsername  N  e  \0 `/ h
BeanMyScope >>>>>>>>> get:scopedTarget.user
4 L6 `. O# p$ a# H5 y+ [, {---------创建User对象com.javacode2018.lesson002.demo18.test3.User@27ff5d15
  K- y' n3 s! p% Y: S$ Z' P, A" I* i76d0e86f-8331-4303-aac7-4acce0b258b8
, l- t  H; K* `' \. U# W+ T; J$ c第3次调用getUsername结束, s' f/ ]' [8 Y- [  m" M/ x
********
1 W' P% c: Q( ^. e$ M2 ]4 L从输出的前2行可以看出:; f0 j' J( n' p: Q5 b
& g2 c7 p; q6 [- l
调用context.getBean(User.class)从容器中获取bean的时候,此时并没有调用User的构造函数去创建User对象, e7 V: H- j+ @- |

" ~* g, m* d" e1 p第二行输出的类型可以看出,getBean返回的user对象是一个cglib代理对象。! ^; n6 ]; `. d! Z2 R2 G2 @

, u" w* i$ z1 K, {2 v& J/ V后面的日志输出可以看出,每次调用user.getUsername方法的时候,内部自动调用了BeanMyScope#get 方法和 User的构造函数。
- N3 A& T0 y* x5 v6 |2 S8 Z+ @2 I; h+ i2 }' ]
通过上面的案例可以看出,当自定义的Scope中proxyMode=ScopedProxyMode.TARGET_CLASS的时候,会给这个bean创建一个代理对象,调用代理对象的任何方法,都会调用这个自定义的作用域实现类(上面的BeanMyScope)中get方法来重新来获取这个bean对象。
" o6 R3 P* g& c0 K/ r  {2 f
0 Q% Y7 ~) I# y' L' z动态刷新@Value具体实现
+ i7 [3 K( b$ G4 [+ r: a* D; s7 z
6 w8 o! S7 j+ B那么我们可以利用上面讲解的这种特性来实现@Value的动态刷新,可以实现一个自定义的Scope,这个自定义的Scope支持@Value注解自动刷新,需要使用@Value注解自动刷新的类上面可以标注这个自定义的注解,当配置修改的时候,调用这些bean的任意方法的时候,就让spring重启初始化一下这个bean,这个思路就可以实现了,下面我们来写代码。0 _( C' y2 ~% t/ y4 m+ \" o
! D8 z3 `; o2 A, Y6 J& J& R+ y  {
先来自定义一个Scope:RefreshScope. ^) r3 h2 n# F3 S- ]4 p$ L

9 x; U$ m7 c4 z7 @. \# k* z+ |package com.javacode2018.lesson002.demo18.test4;, N$ r8 k" V8 g& S3 ?
& R( k. ~' I0 u8 {2 T$ k
import org.springframework.context.annotation.Scope;& i/ \4 V* s$ T0 k' S
import org.springframework.context.annotation.ScopedProxyMode;0 o* t! p& N; H: U- E3 Z( k5 ?
0 P6 Y& ?/ a6 g1 o% @2 S
import java.lang.annotation.*;6 k% v1 v+ ?9 R; P8 |* P8 e

4 Q5 a, f/ }$ Q8 V- P@Target({ElementType.TYPE, ElementType.METHOD})" ]! C0 O8 `- u6 e
@Retention(RetentionPolicy.RUNTIME)
. o5 S9 ]0 a8 z  u; z0 I@Scope(BeanRefreshScope.SCOPE_REFRESH)
0 @# r: x+ I5 ]9 E@Documented9 Y- Y( R. g. i( y
public @interface RefreshScope {
2 y2 L) g: m3 \    ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS; //@1  y4 f- C: Z- J
}
2 A' ?* _; j" g. }$ |; e" X' A- @要求标注@RefreshScope注解的类支持动态刷新@Value的配置" ^* a# [% X; t
) {3 I7 H. R2 d* `
@1:这个地方是个关键,使用的是ScopedProxyMode.TARGET_CLASS& ^7 l; x7 B0 V0 a! p

" r& w( D2 O4 X" v: B1 J; @# ]这个自定义Scope对应的解析类  p+ N) P: p% }. @6 Q& t

* V4 `- C* S: l* V下面类中有几个无关的方法去掉了,可以忽略
9 l9 |! g. i9 L- t6 L2 M  O
2 n  Q) A  _* c+ \% Kpackage com.javacode2018.lesson002.demo18.test4;
+ c6 M) N  a) b; L+ q: J# ^# i/ x* s7 N  I

/ T/ F1 I5 \! z  aimport org.springframework.beans.factory.ObjectFactory;
0 E5 d/ J% t0 f# f, A2 ~3 Limport org.springframework.beans.factory.config.Scope;: i9 Z7 F. E6 U2 T, R* |
import org.springframework.lang.Nullable;- k/ N. B& E% C$ G) d2 C2 t/ b# C8 ?* B
( Y+ X* {% T, O# `7 Z# L8 N
import java.util.concurrent.ConcurrentHashMap;+ U& h& H. |! A: ?5 @

9 N' l1 c. q$ S0 b! S, ?. d' x6 d% kpublic class BeanRefreshScope implements Scope {
0 ], b3 n" P5 v* I
- X, v0 n9 U/ ~; e% E" x, N    public static final String SCOPE_REFRESH = "refresh";
, t- E4 A; f" l4 l1 U( O! Z
! x4 D3 `: \4 B& p! @7 o0 g4 k( v    private static final BeanRefreshScope INSTANCE = new BeanRefreshScope();
+ R: U" I1 I5 T  U  @% O
7 H- T# s9 E" M# V7 f; w" l- b    //来个map用来缓存bean& n$ w  D( J  i0 T8 s
    private ConcurrentHashMap<String, Object> beanMap = new ConcurrentHashMap<>(); //@1
' c& Q) R7 t1 t) h9 j& {. G' ^2 _: E0 h; F, t
    private BeanRefreshScope() {
6 Y, ^, ~+ u' @    }5 K3 |! n. ^# k4 I# G# }6 h/ @1 Z# d

) V$ o, y; P8 |0 `    public static BeanRefreshScope getInstance() {
+ q4 \# J, p" L" T/ }5 s$ _        return INSTANCE;
7 ]  s& J# \$ z' W# P    }
1 M! H6 q7 }9 d" x2 \1 u3 M+ ~3 {
4 c5 E& h" [7 o7 o$ }' s3 e  H    /**$ `" ~9 j/ {. u/ L
     * 清理当前" X- m; q1 l; ?" b
     */& N$ B$ s3 [- Z8 O/ {
    public static void clean() {
8 h# P, B# V0 k% K1 v4 g9 t1 j        INSTANCE.beanMap.clear();
) K# e# t+ ^6 b' W1 Z1 t# Y4 p    }0 j4 n5 W* {9 H6 ^

# _1 B- B' Y/ d3 [5 p; u+ U0 ?    @Override
4 }/ }, T1 L; I0 G: e2 j    public Object get(String name, ObjectFactory<?> objectFactory) {9 ~% j8 }' P" q# W, d( g" b
        Object bean = beanMap.get(name);
8 j9 i  O& u6 T* _# X        if (bean == null) {
+ @9 c) L# O: q            bean = objectFactory.getObject();
( o8 B. b" h# \            beanMap.put(name, bean);' V- {3 @  E: l
        }
- p" s' ^! g5 ?. K0 p8 B9 d: e1 b        return bean;& @) ?# z9 Q+ z* h/ ^! Z
    }, U& b& S% ^! Z" q1 N( i+ {2 o7 ~
4 v( \0 H, P) ?5 w
}/ f+ p5 p) S' w: X+ _
上面的get方法会先从beanMap中获取,获取不到会调用objectFactory的getObject让spring创建bean的实例,然后丢到beanMap中% x  g4 u8 x* I  e) x( o. W, d1 }

* h9 S, G( _! |  _( K* R3 n上面的clean方法用来清理beanMap中当前已缓存的所有bean
" E; f4 l. e# J$ o& Y! R9 R) u7 \' R! h$ h# }
来个邮件配置类,使用@Value注解注入配置,这个bean作用域为自定义的@RefreshScope1 |- W- o( E( G* N, i

  L! Z3 u9 V0 N- I8 vpackage com.javacode2018.lesson002.demo18.test4;
$ ^: t, p/ J/ t2 ], F
; K% N* v* a7 kimport org.springframework.beans.factory.annotation.Value;! g/ E0 h0 w" K. }' M
import org.springframework.stereotype.Component;
' W: I5 G  I" a2 r* n5 x4 J" l* s7 l
( z# ?- ?, F$ f/**# W8 E4 F. X. G1 Y' \: f7 S
* 邮件配置信息
- K+ Q- ?$ Q/ n9 N, J */
2 P! X9 z% X9 W/ P! t@Component4 @" v6 l7 j4 Y8 p( a% k4 L2 L
@RefreshScope //@1( v0 Q6 w9 j/ S' j5 O- l
public class MailConfig {6 d0 T8 L* M) w3 o0 ?, M9 G
: V" A" x. D0 ~0 n3 Y2 g2 K
    @Value("${mail.username}") //@2
% N6 i7 ]$ R6 e" |4 \- v- }6 U/ k    private String username;
) {4 `  O9 n; N: d1 }) I1 E- y, I' P+ e* H
    public String getUsername() {
9 D) _, h( d( O- E9 `. M, U8 k9 V+ q        return username;
2 T6 |2 E( X: ^# A  s8 p# c* N    }
2 q/ O1 {+ R: b  J- {& V, b8 R  M2 ~$ M4 Q8 c
    public void setUsername(String username) {8 J7 g, \4 ^! I" x+ x
        this.username = username;( N) w1 s2 W# l# M( g
    }/ l4 j& o# n; R: ~: n9 I

; n, e" y2 r: E/ X7 p0 Y' j# U1 q    @Override
' x' m: \' R: c7 @* d" O" m    public String toString() {) u& L& I, ^$ E+ V1 U6 K
        return "MailConfig{" +8 B) w( S% ?5 Q! ~) j" x
                "username='" + username + '\'' +, ^! F9 u$ q) v; W  Q
                '}';$ d# T0 `3 t/ F, x, e. P
    }
! X4 D& G' j  c}; z" X8 k8 l: D' G
@1:使用了自定义的作用域@RefreshScope2 k2 l' ?5 @% K$ O8 P/ L; d

  G! P# q: i# q( i  B4 g@2:通过@Value注入mail.username对一个的值
# A( O5 @2 W* h( h) k3 d1 K; c" G
重写了toString方法,一会测试时候可以看效果。
9 @: s. V/ a% u1 b$ I" Q. Z! J1 J( s4 [' R
再来个普通的bean,内部会注入MailConfig- u" @' [, p8 k; [. L
/ q+ B1 h6 V$ ^0 S  R) i: U
package com.javacode2018.lesson002.demo18.test4;6 S( f) B; N1 m: _& l* i# J3 ]$ K
' @, R/ `+ L. X5 w6 H: B, c2 @
import org.springframework.beans.factory.annotation.Autowired;# v, P" A, u, A) U* P8 W
import org.springframework.stereotype.Component;# K$ ~. I% d; x+ _2 M' I( {
+ I3 k' _2 D8 S
@Component
7 V9 I, I' }9 Q. I! cpublic class MailService {
( |5 k2 o: N" Q( D9 c- l( [- Z( N1 O    @Autowired( z' r3 V$ b( w4 K
    private MailConfig mailConfig;$ `2 N5 ~  w- ], z6 r% m. R
0 R9 r) n7 v- l' s; L
    @Override
$ O2 y$ ?: S- h& r4 i% L! T. n    public String toString() {+ x1 k$ v5 w, O2 c' b6 i' |
        return "MailService{" +
: Q4 R, p' M" x                "mailConfig=" + mailConfig +
) F6 h* f; G2 Q9 g9 @- f' r8 K                '}';
+ T" _5 A6 \& [  C  v5 p    }2 F$ I+ K/ ~4 y; X6 s0 V) k5 R# \
}5 s. u+ b; w" Y  c: l
代码比较简单,重写了toString方法,一会测试时候可以看效果。
2 n' D2 c$ q4 Y( b& @( B
3 F+ n& @4 K- f' g来个类,用来从db中获取邮件配置信息: q6 v" H2 c/ y9 m- z, v
4 v1 ^) W/ W7 S6 V6 \
package com.javacode2018.lesson002.demo18.test4;
* x: E" A( R2 m$ }' N5 J& m. N$ C. \2 S+ R) W0 W
import java.util.HashMap;
) v2 V0 b- t8 Mimport java.util.Map;
0 e3 u7 s4 f2 _: F" K* f4 z- u1 Yimport java.util.UUID;
( J/ R7 R0 E. g" d' b
8 I; @, P3 Y+ ]7 Opublic class DbUtil {
! f3 ]6 e  a, _5 H    /**
. L/ @' w$ i$ ~$ Q     * 模拟从db中获取邮件配置信息4 v0 y; O, B+ I+ d1 }2 ]6 |* {
     ** f  p* f8 C" [# X
     * @return* Q$ B3 J2 N- O$ l( N. ^" b
     */
5 \( o8 S+ b8 V4 a  L    public static Map<String, Object> getMailInfoFromDb() {  c% F+ z2 g  ?, f" s
        Map<String, Object> result = new HashMap<>();
4 J1 D* l' q" R( o2 V( O        result.put("mail.username", UUID.randomUUID().toString());
, G( N5 M  ?# k        return result;- k7 Y$ I# {& u2 E. l, b
    }1 Z7 `0 Y: n: n- V2 I
}9 }6 V  `5 W0 Q4 _5 q
来个spring配置类,扫描加载上面的组件
0 v4 }5 I  U3 L2 j! g* F7 e& F9 `7 K
package com.javacode2018.lesson002.demo18.test4;
, U6 o9 N9 q$ J
2 j* S1 E- L* T, W9 B6 H: gimport org.springframework.context.annotation.ComponentScan;
0 n  _, a: e  ^/ Q9 r5 l9 b$ mimport org.springframework.context.annotation.Configuration;* v3 U. O$ A: j9 k) Z! N

5 U1 x6 {0 H- F8 D@Configuration, w8 v* y0 I) R5 `" r
@ComponentScan
+ Z7 Q& h5 Q8 Q* r& C5 k: n6 Opublic class MainConfig4 {8 Q8 K* |4 o) \, @" O, U
}
+ C: K  f0 n% i/ U来个工具类
9 B- V( z$ E) n' x0 O% U) g, X( |& x8 [$ O1 P4 s4 D" R
内部有2个方法,如下:
0 k) |, B7 {! v1 x3 S& q& v8 L8 C! s& D9 b, g- s
package com.javacode2018.lesson002.demo18.test4;& M7 v5 }" @; u- ]& R
# g+ W' [9 b/ W
import org.springframework.context.support.AbstractApplicationContext;4 a0 I: ~8 X* Y3 s( \/ I5 u
import org.springframework.core.env.MapPropertySource;
# Q2 c8 Q2 K# y( @; \! V/ |( L7 t0 v9 z0 f& K
import java.util.Map;
/ f5 M5 C7 b" ?4 U& c) q7 f
8 Q* G9 L0 Z- N2 I8 v& ]0 M; Ypublic class RefreshConfigUtil {
& v; Q/ @2 v3 Q2 `) q. |    /**  _, f) w* p+ B1 n
     * 模拟改变数据库中都配置信息
3 A/ E+ C  x, r$ l4 Q) ]     */
$ @2 e- B1 i- y' }$ B4 ]    public static void updateDbConfig(AbstractApplicationContext context) {% k1 s" s; x# I
        //更新context中的mailPropertySource配置信息7 z' n( ^6 v' [0 d0 B7 K
        refreshMailPropertySource(context);. L1 F- Q- u% Y
% x+ {0 f5 I6 |0 P8 t5 W1 v  q
        //清空BeanRefreshScope中所有bean的缓存  @7 f2 P% O8 e0 F' o
        BeanRefreshScope.getInstance().clean();
5 @3 L% ^5 ?& _: S$ [$ B+ d; c    }3 t: H0 I+ o" D" m+ g
. q( x! u6 X7 L% I" i& M
    public static void refreshMailPropertySource(AbstractApplicationContext context) {
8 Z7 W) A- I$ R7 ]' y7 A7 n        Map<String, Object> mailInfoFromDb = DbUtil.getMailInfoFromDb();. H  c) L- P+ o5 R
        //将其丢在MapPropertySource中(MapPropertySource类是spring提供的一个类,是PropertySource的子类)
& a+ |, j% e0 S        MapPropertySource mailPropertySource = new MapPropertySource("mail", mailInfoFromDb);
1 L" f- V8 H7 E' @1 t$ \        context.getEnvironment().getPropertySources().addFirst(mailPropertySource);9 C; S2 e7 `# _( u
    }
, ]6 {! M( C$ [3 l: M7 e- m( P
/ v5 Z3 O3 y4 G}
* A  K0 o+ ^3 n  t9 O8 ~updateDbConfig方法模拟修改db中配置的时候需要调用的方法,方法中2行代码,第一行代码调用refreshMailPropertySource方法修改容器中邮件的配置信息: ~# N+ }4 B2 I2 w

% q- j7 F2 |& K4 E+ K6 V9 U7 ABeanRefreshScope.getInstance().clean()用来清除BeanRefreshScope中所有已经缓存的bean,那么调用bean的任意方法的时候,会重新出发spring容器来创建bean,spring容器重新创建bean的时候,会重新解析@Value的信息,此时容器中的邮件配置信息是新的,所以@Value注入的信息也是新的。/ t9 K2 _) e! y# q$ Q- }3 w
% \8 w& i0 w; |$ _9 u0 \
来个测试用例! P- Q! ~2 g# i! o5 X( P' H

5 }3 i' g% w/ j7 r@Test
& e1 {0 G* \. \* E- R' y* E; lpublic void test4() throws InterruptedException {" E" \9 {" ?$ T0 w' D  ~- J
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
; b& s  L6 h& C8 m# r5 k$ N5 w, ?    context.getBeanFactory().registerScope(BeanRefreshScope.SCOPE_REFRESH, BeanRefreshScope.getInstance());# L' Y3 {! R$ W6 X. n; i
    context.register(MainConfig4.class);3 y" x' D2 `3 y4 `' ]- t, R7 P
    //刷新mail的配置到Environment
- p3 }+ J/ Q/ ~/ q/ p  o    RefreshConfigUtil.refreshMailPropertySource(context);5 C6 R  `& p( m4 m+ G& i
    context.refresh();8 V; [0 c( u7 N* k+ T5 n) I
: w/ Y( L9 m0 ~. U0 N0 |% b
    MailService mailService = context.getBean(MailService.class);
5 o% X; z+ ?# Y3 p$ S/ R3 g; ?  ]    System.out.println("配置未更新的情况下,输出3次");+ O4 m2 `, P$ F, B
    for (int i = 0; i < 3; i++) { //@1; e3 A1 L7 U9 Y; {3 i1 N
        System.out.println(mailService);
& p: ]: K! N7 }/ o        TimeUnit.MILLISECONDS.sleep(200);
8 [3 _0 m; \. M: W/ J  |2 S    }
$ B, {$ \2 |6 B* J# D, W$ I0 B9 L
    System.out.println("模拟3次更新配置效果");
8 [: N) x2 z& [% R' M$ A    for (int i = 0; i < 3; i++) { //@2: z( J0 v8 C% Q- f$ x* U
        RefreshConfigUtil.updateDbConfig(context); //@37 H3 C$ D9 W6 \, Z' Y
        System.out.println(mailService);
% r4 E' J" P1 }3 Z' C6 w8 P) i        TimeUnit.MILLISECONDS.sleep(200);
1 ?: C: i. N+ n) R) w    }
0 h% w5 K9 B' @2 {7 t+ R& u}
# f6 z* T* Q: b  H* @@1:循环3次,输出mailService的信息
' k7 u2 v5 u0 r* ~3 V2 C' E
, y+ x  ]0 f. W  Y@2:循环3次,内部先通过@3来模拟更新db中配置信息,然后在输出mailService信息7 I9 D1 O: x! V# B( \  i  c+ [  t" ?! [

# o6 w9 N# j: d- C+ ?3 M见证奇迹的时刻,来看效果
' {& s7 d- Y8 }: N( U( K' V6 z* \; V
配置未更新的情况下,输出3次
9 }5 f2 H1 A; {8 k2 t; I1 IMailService{mailConfig=MailConfig{username='df321543-8ca7-4563-993a-bd64cbf50d53'}}8 v- ^) c4 V! s1 k1 ^( n
MailService{mailConfig=MailConfig{username='df321543-8ca7-4563-993a-bd64cbf50d53'}}
; S& _4 }+ w% u7 z( J: QMailService{mailConfig=MailConfig{username='df321543-8ca7-4563-993a-bd64cbf50d53'}}
# f, o! \' k, z3 o0 ]. [5 n模拟3次更新配置效果
4 D3 R4 K8 L$ y) I; j7 JMailService{mailConfig=MailConfig{username='6bab8cea-9f4f-497d-a23a-92f15d0d6e34'}}( z0 j& t& n( Q3 h4 `* y" y9 O
MailService{mailConfig=MailConfig{username='581bf395-f6b8-4b87-84e6-83d3c7342ca2'}}; k0 T& m$ J& `  Q. S
MailService{mailConfig=MailConfig{username='db337f54-20b0-4726-9e55-328530af6999'}}
* j5 m( X9 ~6 C' k/ I1 ^上面MailService输出了6次,前3次username的值都是一样的,后面3次username的值不一样了,说明修改配置起效了。
* F" i& M* Q4 s1 R. U! H' l; m" T4 H2 Q
小结
. L7 n2 K& x: z, S) g2 N0 _$ u! o5 e, n3 C8 {: P& n7 t
动态@Value实现的关键是@Scope中proxyMode参数,值为ScopedProxyMode.DEFAULT,会生成一个代理,通过这个代理来实现@Value动态刷新的效果,这个地方是关键。- b6 ^" d: J, V( e* h  q8 ?
, C+ \2 x1 ]1 L$ p# c
有兴趣的可以去看一下springboot中的@RefreshScope注解源码,和我们上面自定义的@RefreshScope类似,实现原理类似的。/ f7 M; e* m% \
! }2 g2 p8 ?* F6 ~5 X
总结
9 ]4 l8 Q$ i7 O) ^# Y2 ~9 M# g9 a  ?; h7 y, H& b
本次面试过程中3个问题,我们都搞定了,希望你也已经掌握了,有问题的欢迎给我留言,交流!
, u& ?2 Z4 h$ Z$ w! f. f; b5 U- @2 G5 ~8 S. s. Y: ~2 K0 p
案例源码5 \* b2 v$ Y% H3 h' s% ]

' ^& t( x# I% |( o& w' X5 x$ Mhttps://gitee.com/javacode2018/spring-series% z" j6 E4 F1 w, O
路人甲java所有案例代码以后都会放到这个上面,大家watch一下,可以持续关注动态。4 D- ^- k" y9 E! k8 ~

" r& t+ j/ ]8 I+ D! k, iSpring系列
! Z8 w7 F* q* Q% B6 w
8 @3 m" E7 C' ~5 m  a% MSpring系列第1篇:为何要学spring?1 `: u" r( ?) W% n, I: @; d* ^

' s6 V0 d2 t' `. Q  \Spring系列第2篇:控制反转(IoC)与依赖注入(DI)) h- I1 u( |; H7 R* X; B' ~$ f
& A: A8 G, q) H& F% I  O
Spring系列第3篇:Spring容器基本使用及原理
- y4 A- t. n, A' G  j- ~, Q
+ u6 x% m8 \+ \2 OSpring系列第4篇:xml中bean定义详解(-)
5 {- z# U% O, s5 `! k" {
6 F* e! P  q7 ^  j. Z7 ~* iSpring系列第5篇:创建bean实例这些方式你们都知道?
# [& T6 s. P: ?2 C# A
% R3 Z, K! @  C. ~/ I3 vSpring系列第6篇:玩转bean scope,避免跳坑里!1 ]' c, ?+ t3 b9 @) ?$ `0 V# o

6 g# A. G& g/ y1 K. USpring系列第7篇:依赖注入之手动注入
& \# O- V7 ]. d- r
3 P4 s' L# |& i% R: \) k5 dSpring系列第8篇:自动注入(autowire)详解,高手在于坚持
9 ]0 }* i9 G3 G
0 n4 @4 D/ e* _. CSpring系列第9篇:depend-on到底是干什么的?3 G; O3 ~# E7 d# x+ J- f# B. B( u
# u5 a0 `0 P6 o. G; r1 @
Spring系列第10篇:primary可以解决什么问题?
% M9 e# t3 W8 N5 q2 X4 c  }& f) o- j" O
Spring系列第11篇:bean中的autowire-candidate又是干什么的?# n; V9 K( O9 F& f' d

+ m+ @' b6 F5 W; ]( oSpring系列第12篇:lazy-init:bean延迟初始化1 h  l) {9 K5 W4 V: W2 U0 a

( J! l! @) l  R5 \% NSpring系列第13篇:使用继承简化bean配置(abstract & parent)/ i* k& j) u, x  b$ y
4 \! n) Q# i) Y3 D9 w
Spring系列第14篇:lookup-method和replaced-method比较陌生,怎么玩的?7 S" r" m; g! m$ e, }* ?+ S
( X4 |% K; u6 x+ a/ \
Spring系列第15篇:代理详解(Java动态代理&cglib代理)?
) m, X# p& w1 i; R7 N+ h, j/ i( h$ t) [+ z1 W/ p! Y% ^# j
Spring系列第16篇:深入理解java注解及spring对注解的增强(预备知识)
+ Y% _) M- C8 x/ @1 b! @! \/ n" v+ c  A; @8 |& `  D! j. F
Spring系列第17篇:@Configration和@Bean注解详解(bean批量注册)
' L' {# |( f0 b$ l# e
6 r* E* K5 S5 y9 ^Spring系列第18篇:@ComponentScan、@ComponentScans详解(bean批量注册)
9 R% Q7 |8 F8 D) H8 ?
) a; C! L5 n4 C% F  aSpring系列第18篇:@import详解(bean批量注册)
. M6 [, ]9 B0 O4 T$ k+ G. @) N( I- n
Spring系列第20篇:@Conditional通过条件来控制bean的注册
1 G4 \, Y3 X0 G! L6 R5 C' j+ H3 _* ^3 E" G9 f% n& F" p# [
Spring系列第21篇:注解实现依赖注入(@Autowired、@Resource、@Primary、@Qulifier)! t% S8 W. h, w9 E% m; W

2 f4 r6 t  `. J: T' D$ b- MSpring系列第22篇:@Scope、@DependsOn、@ImportResource、@Lazy 详解" S8 s: F5 x; Y& A% ^8 M
  Z  {9 D- a( C7 e1 w2 U
Spring系列第23篇:Bean生命周期详解3 a6 K% L: P/ s6 M+ B+ D/ y

0 W* |/ B: M) o" B+ v* t4 M/ ZSpring系列第24篇:父子容器详解
2 i, l% ^4 W; I# n0 R1 Y
. b4 t8 S) r, y; P* `) D更多好文章
, j6 N0 @* \8 D4 _
0 S! g+ ]: [" k; i. F* OJava高并发系列(共34篇): x% r( B( t2 R- _+ Z

1 Z$ p3 s% X. FMySql高手系列(共27篇)
2 s; [2 p  T) x/ P; _) r8 u" X
1 Y) f4 E5 |, _7 q5 }6 {5 D: |Maven高手系列(共10篇)
! D8 h; Y+ H! U# U- e6 V- Q3 N9 n: Z; c* A- i" F0 X
Mybatis系列(共12篇)$ `0 E# y0 D; A) p: y2 l) o; h

# o, L" f+ J0 [9 j" m9 c3 Y聊聊db和缓存一致性常见的实现方式: I9 E" n! t! A* }6 r" Q% A
/ Z6 A7 b1 f* _  t  P: n) ]0 Y! t
接口幂等性这么重要,它是什么?怎么实现?; Y0 J1 {1 w. H( P3 Q+ w! k' C
/ Y# o& D5 Z4 z/ s+ Q' k' c
泛型,有点难度,会让很多人懵逼,那是因为你没有看这篇文章!: d, r' `4 [( D( Y+ G: p+ k& ?
————————————————0 c' Q/ ~, M# ]) J6 u" O( I( U
版权声明:本文为CSDN博主「路人甲Java」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。- X# n3 B) p, V+ h. D& l5 S2 k
原文链接:https://blog.csdn.net/likun557/article/details/1056487570 |% K; Y) ^% J# v8 o1 ~- B

2 w; l3 m) _! l$ D# L9 J$ ^$ \+ @1 N% J  O  O





欢迎光临 数学建模社区-数学中国 (http://www.madio.net/) Powered by Discuz! X2.5