数学建模社区-数学中国

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

作者: 杨利霞    时间: 2020-5-23 11:01
标题: 太狠了,疫情期间面试,一个问题砍了我5000!
太狠了,疫情期间面试,一个问题砍了我5000!
/ \1 Q3 A. u2 q" P/ }7 Q) K疫情期间找工作确实有点难度,想拿到满意的薪资,确实要点实力啊!3 C/ r- n/ R" M1 J0 M" j
) n+ P* t1 l/ Q
面试官:Spring中的@Value用过么,介绍一下0 }3 m+ |6 _( U

. w: m! ~' z9 n6 M. w4 y; {- s5 z我:@Value可以标注在字段上面,可以将外部配置文件中的数据,比如可以将数据库的一些配置信息放在配置文件中,然后通过@Value的方式将其注入到bean的一些字段中: E! ~) r7 `& n7 ?7 y0 J4 h
( N+ ?" M0 D7 b6 J) ]
面试官:那就是说@Value的数据来源于配置文件了?
/ q% o; R6 N) l/ @. m: w, H" J$ I
我:嗯,我们项目最常用更多就是通过@Value来引用Properties文件中的配置
; U4 \. m* e, x# T, U& p
4 S) k6 a1 v/ R! g9 t面试官:@Value数据来源还有其他方式么?
, b$ }1 B7 q6 y' n7 F$ o2 a# k, I. Y6 z% }# o7 P  w! |
我:此时我异常开心,刚好问的我都研究过,我说:当然有,可以将配置信息放在db或者其他存储介质中,容器启动的时候,可以将这些信息加载到Environment中,@Value中应用的值最终是通过Environment来解析的,所以只需要扩展一下Environment就可以实现了。8 ]* [( Q" E! K% \1 ~- z& O

, d# J* g2 b/ @) \  Z* W' m面试官:不错嘛,看来你对spring研究的还是可以,是不是喜欢研究spring源码?6 T& m- Y( a6 r+ g  O; ~& Y7 K

7 |4 p: l8 _: Z; a$ l我:笑着说,嗯,平时有空的时候确实喜欢捣鼓捣鼓源码,感觉自己对spring了解的还可以,不能算精通,也算是半精通吧
. O, ^! ?" E, o5 o0 E; g, }* a% p# y) [; `6 y  L" a
面试官:看着我笑了笑,那@Value的注入的值可以动态刷新么?
/ v0 U) ]4 a! v% k& t$ x" N1 H/ ~: V
/ z8 M/ a3 p+ O, m我:应该可以吧,我记得springboot中有个@RefreshScope注解就可以实现你说的这个功能. a7 [$ ^& g; O" G8 B' A
; Q6 s. ]' }$ ?
面试官:那你可以说一下@RefreshScope是如何实现的么,可以大概介绍一下?
, T  G1 S& Q0 ]9 G7 u3 N# I3 }, G: d8 X% k% P
我:嗯。。。这个之前看过一点,不过没有看懂% u! {4 j7 ?7 L/ L' W* K- m; X

8 e; A% k1 v3 @2 A, M面试官:没关系,你可以回去了再研究一下;你期望工资多少?8 b* \8 B2 C. }# R# A

+ G1 `! g/ [* z7 S我:3万吧, F& z7 ]2 z' u' c

& E2 _" C8 {4 G0 X# k面试官:今天的面试还算是可以的,不过如果@RefreshScope能回答上来就更好了,这块是个加分项,不过也确实有点难度,2.5万如何?  [; _) B( }. M/ y0 {! y; q) e, s
& Y: J/ T% k6 R# Y
我:(心中默默想了想:2.5万,就是一个问题没有回答好,砍了5000,有点狠啊,我要回去再研究研究,3万肯定是没问题的),我说:最低2.9万; d. Z. I" K0 \" V
% q: N1 I3 I/ w( k
面试官:那谢谢你,今天面试就到这里,出门右拐,不送!9 o9 `4 u; A3 c7 u3 d# d

/ E! y8 S3 \9 C, ~我有个好习惯,每次面试回去之后,都会进行复盘,把没有搞定的问题一定要想办法搞定,这样才不虚。( a7 D# @% \. ^+ l3 j" V8 c8 r5 f

0 a* x' h0 i' [: i6 `这次面试问题如下
* e' W8 Z/ o8 T. ~: z
# H$ ^& v: M6 ^3 i9 r8 v@Value的用法
% W1 u+ V. s* {9 S3 N; p( i
6 U- I: a* T6 Z@Value数据来源! q/ p, o8 l, r+ i
2 |$ K4 m" e9 i& _3 z& V
@Value动态刷新的问题
- p, Y8 v' V& V) c" g2 Y
& {# K2 k% @" O; k- e1 x/ U下面我们一个个来整理一下,将这几个问题搞定,助大家在疫情期间面试能够过关斩将,拿高薪。
0 @+ B) _) Z4 f3 x( r
  _* m3 |/ s/ V0 t2 X7 I@Value的用法" E+ l7 z& k' B

: i' `: O! ~/ X! H系统中需要连接db,连接db有很多配置信息。( D$ u6 v. ]) a5 H: a% k

$ l$ C5 B6 @1 ^2 `: s7 g系统中需要发送邮件,发送邮件需要配置邮件服务器的信息。
0 M3 ?: t! k  W% X
7 F) }, y1 x1 w2 N还有其他的一些配置信息。  `+ p6 x5 l9 g2 d' J6 U. Q/ E6 r6 v

, X8 c: f( b1 O: Z; f我们可以将这些配置信息统一放在一个配置文件中,上线的时候由运维统一修改。1 k) ^, C& Y7 x: e( O/ r
% K7 A4 _2 M: A4 ~: Y8 r
那么系统中如何使用这些配置信息呢,spring中提供了@Value注解来解决这个问题。
. Y! |( U. B5 x
# O4 m! L' i* U" j+ m, y, y% k) O通常我们会将配置信息以key=value的形式存储在properties配置文件中。
- [6 `- ~/ t, L( F, D, K/ g( `8 h3 o- i+ {0 e
通过@Value("${配置文件中的key}")来引用指定的key对应的value。/ U: i# {/ {0 F. X( t* m
2 }- \* y) S: {& E  G
@Value使用步骤
3 R% w" p# Z, Y+ ]* s4 K% @: ^7 u
7 c! ^* s# r. ~3 c  p" J  M' w步骤一:使用@PropertySource注解引入配置文件
  z$ S% T% ^/ ^/ x  r3 I% b
: e7 L  l7 O) e, J将@PropertySource放在类上面,如下
5 D; w7 Z  }% P& m) @. J4 o- K, d+ [0 [6 ?! h
@PropertySource({"配置文件路径1","配置文件路径2"...})& S1 V- S5 i! r
@PropertySource注解有个value属性,字符串数组类型,可以用来指定多个配置文件的路径。
2 ^9 o. b' z7 V! ~! D1 T
5 f! x, m, d. H$ R$ i9 c; a0 Q如:
8 `* j; M: O9 [- }/ a, g% s
, z  Z' B" _: W+ m6 b: W; N# F@Component
2 p3 e' U+ f8 @* I) `@PropertySource({"classpath:com/javacode2018/lesson002/demo18/db.properties"})
! Y; E4 x- s( e4 B2 _/ t! qpublic class DbConfig {% {( I) o7 }) A+ ?4 v% m/ @
}1 m5 L, C8 n  C7 |' u
步骤二:使用@Value注解引用配置文件的值0 d3 Y- |9 _5 R
& A  x  a$ ?) N' W/ H
通过@Value引用上面配置文件中的值:* s! H1 J+ ?# n2 J9 k$ r1 d

. }* d+ w$ Z* d: `语法) ~* b: [# j1 d+ l, Y8 _1 [: y. @
1 b% b* X& U. h
@Value("${配置文件中的key:默认值}")
# S5 C# m) s' ~; a/ i@Value("${配置文件中的key}")1 p- v$ H: o1 L" C
如:
$ B/ S2 D# Q" U: e+ ]
5 ?) |! n8 O, s@Value("${password:123}")
. v- }2 g, y9 r6 T# h5 P6 {上面如果password不存在,将123作为值
. [1 w. y! \& ^/ a4 c. W2 x/ I: }
7 }: M3 k1 v9 u3 ^) L@Value("${password}")
; U; c) i6 h' c( `上面如果password不存在,值为${password}
0 Z2 k4 J9 h: v: `$ g0 r9 B$ p: D% Q
假如配置文件如下
4 N2 c( S4 J% W) D5 X2 ~
* D4 }9 V/ U5 Q$ W' [* Djdbc.url=jdbc:mysql://localhost:3306/javacode2018?characterEncoding=UTF-8
5 J$ A. _: L$ Y6 d: Rjdbc.username=javacode' \( g& b( }0 W2 E: b
jdbc.password=javacode" H5 f2 A. f- ~3 s
使用方式如下:
% Z- ^! D" C6 D: E, h: y7 E
1 }( ]1 l1 y2 y, p6 K+ y@Value("${jdbc.url}")8 ?! |& a( f1 m( ]
private String url;
! u2 v# m" K  o) K' E4 q
7 _" I3 n* i/ ~. e+ x* w; W- Y@Value("${jdbc.username}")
1 C# a6 _8 }9 F1 Bprivate String username;! \4 _1 b0 _3 B
8 M, |& X' Y4 O! W- O
@Value("${jdbc.password}")0 b* z2 B0 U) u7 _
private String password;" y- ]* |1 G7 Q( I6 O! x
下面来看案例( j. g( _6 j3 q; q% G& D
% J2 \! @2 T' D" T; v; f  C
案例
9 ?/ j/ c5 S' A5 L& n4 J
3 [, W( Z. f9 N8 h" R; Y. X# N# L来个配置文件db.properties+ q6 z2 u. d: t5 B. S, E4 L
: s( ]* k) E  c# ~+ ]
jdbc.url=jdbc:mysql://localhost:3306/javacode2018?characterEncoding=UTF-8
$ @; G6 e, ~# W/ p6 Sjdbc.username=javacode: `+ V% @8 ?$ t3 |% z& t5 B
jdbc.password=javacode; F) r2 }; u+ r8 W6 z* G
来个配置类,使用@PropertySource引入上面的配置文件
) O" S  c: B% m1 h& s/ y
9 H# d1 c( p. h" i, mpackage com.javacode2018.lesson002.demo18.test1;
7 ~, [* @. A, h) t$ O
8 @! T8 L/ ~( Q6 z% z. Eimport org.springframework.beans.factory.annotation.Configurable;- }* j8 |: j, b& T3 i
import org.springframework.context.annotation.ComponentScan;
9 z/ u7 v3 |* r3 U( {8 }8 k" Qimport org.springframework.context.annotation.PropertySource;
: B$ O! N& ]" H. j* T4 c. h4 Q) g6 H
@Configurable
& U, M- Y; {! d- u3 h@ComponentScan
9 z2 l: [3 T+ K2 D* D. A@PropertySource({"classpath:com/javacode2018/lesson002/demo18/db.properties"})
5 K# O8 q) U, ^# n+ l$ G- i9 |+ h, kpublic class MainConfig1 {8 L! E. Y8 x3 W6 `) A
}, x$ N( b. ]5 l3 h4 T5 o4 _
来个类,使用@Value来使用配置文件中的信息
8 ^2 Q  D. b, J* r7 d3 H5 }2 _% e6 X
package com.javacode2018.lesson002.demo18.test1;
4 E6 r" l9 g! N( z  n4 F1 {% H- m5 @: e
& y6 G5 X& b$ q; Ximport org.springframework.beans.factory.annotation.Value;
- j/ Z+ }8 U: h4 b' ximport org.springframework.stereotype.Component;
1 B! F7 Z8 t9 H9 I- p
. Y2 ^7 V1 k4 C/ b/ I3 j( B) E@Component9 S7 t% |( a( U5 z0 P% ~
public class DbConfig {4 Y' ?. v7 [7 Y* e3 a

% f( Q' ~8 r" v; ?    @Value("${jdbc.url}")
/ T2 Q2 q& b" j0 M$ d# Q0 l    private String url;
, D' D1 ?0 C5 Y/ J
0 U8 d6 e/ z1 ?- f1 [2 p    @Value("${jdbc.username}")
' u* \+ Q1 v* |8 |0 n    private String username;" @  L, {- q! X, N

0 z2 U3 I) v( H* u: L( N/ d8 k3 [    @Value("${jdbc.password}")0 f8 c3 A- N! b
    private String password;
& K( z( v: Q: c/ G. k/ Z% u
  E/ ]1 a8 Z3 L1 j6 C( {    public String getUrl() {
* A7 ?) Y/ A# y8 ?, I7 b7 J        return url;7 k* g9 K# [. C1 E9 `. t# b
    }
) a: q9 D1 F' r# P9 H: t8 @
0 V8 h/ c% F( |# d3 B    public void setUrl(String url) {4 w1 z' Y9 `/ l+ u  Y9 S: X
        this.url = url;
9 s6 A( _$ W$ U- _5 O+ z    }
. K2 E  h" u/ t' k$ i. O/ r+ s/ N
    public String getUsername() {! W7 K3 H  h" E% n/ }
        return username;# P' a' f; O8 v% x% A
    }
" s9 U3 B* t* X) {' D( ?  c3 \2 d" i' Y
: I$ S: ]; O3 s4 e    public void setUsername(String username) {
6 M" h6 ^0 b2 N* r( O& q        this.username = username;, S2 v4 D6 G* r+ n7 Z
    }. I$ N6 T  l) f; q7 S

/ Z! v8 T8 C( E. S+ W7 A    public String getPassword() {
6 ]: M% Z; d0 Q" ]        return password;
+ [$ p, r* V. U2 Y    }
% {& ]7 n  Z% z1 _  C& F5 z0 u) i' ~$ H" w" J
    public void setPassword(String password) {
; h  D1 C# K" S) p( D  ?        this.password = password;, x2 ^+ ~3 J0 z0 o0 i
    }
: \: U! Q4 @- M$ R; b/ P: d8 k' Q$ c4 B2 [0 V
    @Override/ j2 P6 `4 w; `( F
    public String toString() {
8 _% M$ }/ b4 w6 k. e8 T        return "DbConfig{" +  c9 }  i! ]+ |1 o4 T
                "url='" + url + '\'' +
  f1 c  s; y3 }$ M3 O                ", username='" + username + '\'' +
' a; ^4 y2 f, W% R2 z                ", password='" + password + '\'' +* i. d5 o$ p. U' C# J! O
                '}';
# i, }" z$ l* h    }
( |0 T, c6 `! A, F' Z, a}
# o  [3 F# c$ l, ~上面重点在于注解@Value注解,注意@Value注解中的' I+ ?6 v3 ~4 Z- s

7 T6 k" P2 G3 a0 m' d% E来个测试用例! T* g* a% X& }* s0 O) f4 a
- p/ [- h7 T  a! p+ g
package com.javacode2018.lesson002.demo18;+ s$ `$ T. V  H6 k! W
' }) A6 [5 I7 u3 v! n* @
import com.javacode2018.lesson002.demo18.test1.DbConfig;
2 v. p  K3 `7 s5 F6 x, mimport com.javacode2018.lesson002.demo18.test1.MainConfig1;
! a: P: q6 k  U2 i" q) Dimport org.junit.Test;
5 W9 J  T- q' V. p! iimport org.springframework.context.annotation.AnnotationConfigApplicationContext;! I# C/ e: b+ L0 C
  [, f7 Y0 d/ v8 Y) Z7 M
public class ValueTest {) I. [  G% J1 O, Y

9 h* H& `: \5 D1 u    @Test$ Y1 X) L  Q/ B  a  y0 [3 t
    public void test1() {
) d9 R# \* R" c        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();. O3 t6 W: m- T" n( e
        context.register(MainConfig1.class);
4 w0 ]4 J  [2 X$ e8 b        context.refresh();
$ q8 X* |* |  r
0 f( ]) E% A5 G# G        DbConfig dbConfig = context.getBean(DbConfig.class);
% I/ |& D, l1 [' A  O- I  i        System.out.println(dbConfig);0 h% F& w3 S. {" K5 S6 x
    }
9 j6 R3 x/ d0 R" r: l( \}0 ~2 P, `7 }; _9 D6 F, b
运行输出
$ s1 [- j/ c; U4 e; R  q( b9 g
. [- ]% x* @9 zDbConfig{url='jdbc:mysql://localhost:3306/javacode2018?characterEncoding=UTF-8', username='javacode', password='javacode'}
' z/ k* ^% M1 Z, x/ b- z上面用起来比较简单,很多用过的人看一眼就懂了,这也是第一个问题,多数人都是ok的,下面来看@Value中数据来源除了配置文件的方式,是否还有其他方式。
* l6 b) H, T2 T. r: B( s2 K
6 a& w/ j5 l# e@Value数据来源
( t( G* B/ [8 U% P3 X/ }- [
, Y* N5 j0 h& j( o, J/ w通常情况下我们@Value的数据来源于配置文件,不过,还可以用其他方式,比如我们可以将配置文件的内容放在数据库,这样修改起来更容易一些。
/ D. f" N' X& F5 a) N1 l
+ `) p/ G  _3 D. H6 p我们需要先了解一下@Value中数据来源于spring的什么地方。
& m- A5 |& a, y2 `/ \7 b5 I( E5 s& o
spring中有个类
% ~5 m  `- g* V7 t+ B
3 \6 }/ `. P3 ?0 e! u  e' aorg.springframework.core.env.PropertySource' C. t9 R1 T% z9 D1 d7 a
可以将其理解为一个配置源,里面包含了key->value的配置信息,可以通过这个类中提供的方法获取key对应的value信息4 ]  U0 o" s& Y9 E6 C% o

' l3 ]' W0 B! j内部有个方法:7 W& E  J$ j/ W
2 Z# H% \( O* R: ?: h" q! e- s
public abstract Object getProperty(String name);5 @4 z3 e0 f1 E) a8 f% u4 D, I8 [# T& ]5 T
通过name获取对应的配置信息。! P9 D: D* h9 y* F- U  l
  U5 Q7 Q4 z/ {5 |( g0 U$ R& |2 }  `
系统有个比较重要的接口
# q, y/ m$ f4 ]9 @& E' a% ]1 v& a
# c0 C. t) ?% Q  Eorg.springframework.core.env.Environment
6 T- E! z0 Q- U用来表示环境配置信息,这个接口有几个方法比较重要
4 S- l( y: q, O9 T  N' j* E
; @, _+ b& J" q5 |4 }0 UString resolvePlaceholders(String text);; r6 R3 X6 ^: `' B+ v
MutablePropertySources getPropertySources();
( C9 X  f% Y5 E, BresolvePlaceholders用来解析${text}的,@Value注解最后就是调用这个方法来解析的。- U1 ]  O6 L' P" P. N

: c% ]8 P4 ^! w7 M9 `/ P6 lgetPropertySources返回MutablePropertySources对象,来看一下这个类
6 L0 `& P/ w: W$ d
( A$ E+ E7 M3 m2 u: j  opublic class MutablePropertySources implements PropertySources {
. }5 f( L/ b% B8 A' M
* M& Q8 z+ c. S- c6 `( C    private final List<PropertySource<?>> propertySourceList = new CopyOnWriteArrayList<>();
0 N/ F4 T8 i7 z3 V+ C) S( g8 \6 U) Q0 N6 c
}
. d  M; i2 U; D' ~+ X/ ]内部包含一个propertySourceList列表。# u; N' W7 O' i+ Q5 A

  }- N% m1 g; [) Dspring容器中会有一个Environment对象,最后会调用这个对象的resolvePlaceholders方法解析@Value。; d& @- A6 H! O7 C: i, [  g
7 }6 ]! m: A6 w
大家可以捋一下,最终解析@Value的过程:
6 N# Q3 S9 Z/ ]" _6 G. ?4 F( e
7 a$ p5 S, W) q7 Y1. 将@Value注解的value参数值作为Environment.resolvePlaceholders方法参数进行解析
- K4 N, Y6 e8 t8 w* N- `* N2. Environment内部会访问MutablePropertySources来解析
- p$ v8 a7 I/ N7 q! T# [- d3. MutablePropertySources内部有多个PropertySource,此时会遍历PropertySource列表,调用PropertySource.getProperty方法来解析key对应的值( I/ j9 A7 m. k- \7 ]2 [$ S$ |" I
通过上面过程,如果我们想改变@Value数据的来源,只需要将配置信息包装为PropertySource对象,丢到Environment中的MutablePropertySources内部就可以了。
$ @4 ]# S" B* E  y; W  c+ ?9 t+ R: V  v8 M2 V: v. S9 d0 G/ J, O
下面我们就按照这个思路来一个。
  U- E3 F) Z, F. W
1 O/ C! d: f# M来个邮件配置信息类,内部使用@Value注入邮件配置信息
; E2 |  K0 g$ V% V/ [  P
) ^2 D6 _7 h: G, L3 g- qpackage com.javacode2018.lesson002.demo18.test2;
0 S9 x" L) w) g2 i. @1 ~" S2 z2 y: @8 g& |. O
6 G3 q1 P& I& N7 Yimport org.springframework.beans.factory.annotation.Value;  C) q& U% b. t5 p/ o
import org.springframework.stereotype.Component;
4 P0 x. ?5 j# {" L3 Q% B) X/ k3 d- a% M3 u& e7 G' e. c
/**8 L5 U/ P8 E! T$ @
* 邮件配置信息7 H# D9 j9 ?# ]/ R
*/
* O0 a) L: @1 b9 E, F@Component
% U  g3 U7 d% h4 R9 rpublic class MailConfig {; q& G1 q; H. l/ ]1 o, [

8 U$ w' `- X4 \# O" a8 n    @Value("${mail.host}")' B% V) A, `$ \2 G
    private String host;% d3 b4 a5 Q: ?

$ V1 G" T- d: s9 r: `    @Value("${mail.username}")
: b, I3 ~9 L/ Y0 g' w    private String username;
& b1 B5 _  N7 ?6 e& S3 i! W
6 `3 U9 i3 E9 }4 y8 v& N/ k2 H4 \" M    @Value("${mail.password}")
/ A6 U" d" K" ~& l$ i    private String password;
' F. g6 ]" h! w( B0 x; r) r$ z9 L! ]
    public String getHost() {# T: y. h3 z9 k! ^& o9 C9 Q6 l
        return host;
3 L8 O9 C6 J) G0 c" P' g3 f7 t    }
- q# l/ W, e( [# f2 {! w/ {+ M$ F9 Z' _6 j! w
    public void setHost(String host) {
& M9 s% A" E3 l& Y! z6 y+ T0 r( ~, Z        this.host = host;
! a0 o" P1 v: W0 g: y- F    }
2 Q( A$ `, U8 l9 E0 U6 y& p) Q
, z! g, n. P- b) f# \+ |8 L    public String getUsername() {
) a0 s; T) W  H) Q5 L        return username;, _- c. N% \- [3 {0 Q8 E1 R7 S- y
    }+ s' \' }/ C3 u+ B  Q
$ E6 J; n; a! t  y- t
    public void setUsername(String username) {
; m) f, O' z! Q: ?: s9 A8 C3 i4 i        this.username = username;
$ D& ~5 j; Q; {' g/ T    }  Z: G% T4 s: l. t. l$ @1 t, K

% J& e/ E! s* Z- G% _2 t8 L: S* K    public String getPassword() {; x! j# ^" |; e% d
        return password;
; _- h' u, e9 M4 |& ^0 O    }% Z2 o' a% K1 Z* ^
6 {( t! L# F5 Y! h/ z/ |
    public void setPassword(String password) {
6 e; O+ U! H, [        this.password = password;8 i, t7 h4 E6 `3 A6 A
    }
$ c& ~# k' x1 b$ `5 R' ~! h
1 }- U) e+ g- T    @Override# o: k; C6 Z% a* z, p
    public String toString() {7 M( L4 i" h1 w6 D$ h+ F. o) s) G8 H8 Z# T
        return "MailConfig{" +2 u- ^. H/ T7 n$ L/ ~
                "host='" + host + '\'' +6 h! z( c. K$ j( x" Y
                ", username='" + username + '\'' +, e# t/ Y9 @  g  x6 r
                ", password='" + password + '\'' +7 y% a8 {5 b, U- j3 k% C" |; U- a! Q
                '}';  o: |5 f  {5 g) B* L% W
    }+ Q+ Z8 \+ t/ [7 L2 n- ?& |6 `
}
- q* u' @$ N% a* n/ @5 U2 Q$ B再来个类DbUtil,getMailInfoFromDb方法模拟从db中获取邮件配置信息,存放在map中
& T: S' h/ p' w; E* F$ N2 _0 n/ N6 U6 N' j
package com.javacode2018.lesson002.demo18.test2;
0 q, D( a3 I, t) _4 W
8 Y# ?6 I. A" f$ z, B# Qimport java.util.HashMap;  [- u& Y' P% j+ e
import java.util.Map;
* ]: T2 _, ~1 G! T( [8 ?/ k5 U/ ]! F
* Y9 Y9 f7 H1 v( F; [public class DbUtil {
6 J* H* O3 _5 l; m. M! z    /**9 h4 P! D1 V9 D! E* \
     * 模拟从db中获取邮件配置信息  I$ V$ n* X# ?1 ]  h9 t, ~  \
     *
. x8 \9 C- G4 \* H* k     * @return
# U$ ]* ^/ P1 M  F; R6 Z; [     */$ `3 j% z. r( s5 `
    public static Map<String, Object> getMailInfoFromDb() {3 f% ^) R/ ~7 X1 `9 `
        Map<String, Object> result = new HashMap<>();
) k. U& @; z! r        result.put("mail.host", "smtp.qq.com");
. u$ R6 W! x& l6 I0 x; z- a; j        result.put("mail.username", "路人");
% T! E. g& M! ^2 m# P2 M% V" Y        result.put("mail.password", "123");" _+ i' c( z6 H5 b3 N9 D
        return result;; l5 h) N7 i) L7 }; }8 B
    }3 C& Q$ Q1 Q, j. w0 w: u
}
5 w# ]$ x6 j% g' _6 D来个spring配置类
& U4 t5 N3 L. t  e$ A7 g+ H$ `! Q2 |" F0 e$ t
package com.javacode2018.lesson002.demo18.test2;$ C& E2 J+ u& k* R/ ^) \; u

2 l3 [. S! j+ v4 i0 P4 mimport org.springframework.context.annotation.ComponentScan;$ `9 n' L, D5 i
import org.springframework.context.annotation.Configuration;
$ t: r7 X8 c' H+ y) J# }; r3 j( y
, D& E+ S6 V4 d6 v7 ^- T( I2 `2 g9 g@Configuration0 F# A* g1 W9 l  ?/ t
@ComponentScan
2 J- x" u. Q: e1 Cpublic class MainConfig2 {
3 z4 j/ E6 D/ P  Q7 T) N/ G+ {& k}$ p1 `7 b2 \* L- h8 G4 D8 P- U# Q; L
下面是重点代码
9 I/ H3 j5 {2 c. D7 E) ^2 b2 z7 m8 }) _# [
@Test
8 Z6 Q3 S& W1 p' ^% o4 ]; npublic void test2() {
; ~0 L& N) j' X' C  A    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();" N. {- k! ?% V  a) R: R" a" U: i
/ ^! ^% e% }* g1 |
    /*下面这段是关键 start*/  q! R  i) ]$ T* A
    //模拟从db中获取配置信息
- n  A' B- Q3 M: r$ V    Map<String, Object> mailInfoFromDb = DbUtil.getMailInfoFromDb();
) r8 C9 S: p7 h: E: D( n! t9 ]# ]    //将其丢在MapPropertySource中(MapPropertySource类是spring提供的一个类,是PropertySource的子类)
; {. w  U' R5 ^    MapPropertySource mailPropertySource = new MapPropertySource("mail", mailInfoFromDb);$ t8 d7 s2 {  @
    //将mailPropertySource丢在Environment中的PropertySource列表的第一个中,让优先级最高
4 N1 I8 h/ X% \  d0 R/ P, `    context.getEnvironment().getPropertySources().addFirst(mailPropertySource);
) g* i  Y% |+ X/ b& I, q: @    /*上面这段是关键 end*/: B7 m+ ]5 k+ X  f* m

, Y; k- [. B. @' ?4 S- `8 r# C. g    context.register(MainConfig2.class);# n( N! {6 M8 l# X/ |/ @
    context.refresh();* t; d* F' }1 V) v+ K9 J
    MailConfig mailConfig = context.getBean(MailConfig.class);
% G) @: B) D- h+ _    System.out.println(mailConfig);$ P/ g' y* c+ H) B  j
}, Y1 Q% @. J3 a) K0 t
注释比较详细,就不详细解释了。
6 A7 Q4 U9 C" M4 I- ]) {
" Y8 Q9 d( H# ]# F. j直接运行,看效果
5 ]& p" v9 d; ]) [; e: _: q( J, [+ K8 ?1 j) P7 G+ k  ]
MailConfig{host='smtp.qq.com', username='路人', password='123'}
0 a' q2 \  e$ g有没有感觉很爽,此时你们可以随意修改DbUtil.getMailInfoFromDb,具体数据是从db中来,来时从redis或者其他介质中来,任由大家发挥。- A/ s, d: \  C' \$ _( b$ B

: ~) k" o; Y! p( F# ~上面重点是下面这段代码,大家需要理解- N& O# O& K5 C

/ A) r6 _! a1 F& I* J: l" r/*下面这段是关键 start*/0 \1 Q  h8 _6 q( W$ q2 e4 K
//模拟从db中获取配置信息3 H1 B1 @" ~" l- R( H
Map<String, Object> mailInfoFromDb = DbUtil.getMailInfoFromDb();) [( C/ Y  m: |5 I2 S6 l% h
//将其丢在MapPropertySource中(MapPropertySource类是spring提供的一个类,是PropertySource的子类)) q1 i9 r' K) @4 ]
MapPropertySource mailPropertySource = new MapPropertySource("mail", mailInfoFromDb);
9 e4 |3 y) {9 ?# J6 y//将mailPropertySource丢在Environment中的PropertySource列表的第一个中,让优先级最高1 l3 Y, h# s6 o! W* `
context.getEnvironment().getPropertySources().addFirst(mailPropertySource);( ^/ N! @8 H1 R; c" Z8 P
/*上面这段是关键 end*/
: ~3 K% f/ o- s+ e3 x咱们继续看下一个问题
" }3 ]7 A- [3 g: z  H- K7 t$ F* G- S8 o0 V, @) [; y" m
如果我们将配置信息放在db中,可能我们会通过一个界面来修改这些配置信息,然后保存之后,希望系统在不重启的情况下,让这些值在spring容器中立即生效。- l3 U4 d  ]: O4 p, N2 r
, z8 Z" e& S" x! i$ e+ k' n
@Value动态刷新的问题的问题,springboot中使用@RefreshScope实现了。9 X1 b4 Q8 |' u! J
: f5 J3 s# V$ W# G- i8 e. p! b3 Y. S) O
实现@Value动态刷新) R3 f1 B* C' I, T! y# P6 A

4 r) Q% S% Z6 u) f. |/ P先了解一个知识点: c( Q0 B: g3 p0 ?1 _" N! M

6 w5 Q6 g; A# ^* q7 R这块需要先讲一个知识点,用到的不是太多,所以很多人估计不太了解,但是非常重要的一个点,我们来看一下。
, ^3 b7 o# ~# F: \. b" B3 z2 U' y2 u
5 E1 X5 W8 x# B" B这个知识点是自定义bean作用域,对这块不了解的先看一下这篇文章:bean作用域详解
  n( E6 |1 O2 w& N" d- O
2 _& X' N& {0 q/ D! u. rbean作用域中有个地方没有讲,来看一下@Scope这个注解的源码,有个参数是:( r' d+ c( Q7 u3 A' P' [5 J: ]7 c" c
; ~5 H$ h3 |: \8 x
ScopedProxyMode proxyMode() default ScopedProxyMode.DEFAULT;0 N" u& T1 q: t9 F) Q
这个参数的值是个ScopedProxyMode类型的枚举,值有下面4中
' T& i' t" v# r. j  l1 y0 r( B% e# L5 U2 n& U2 U
public enum ScopedProxyMode {
; c' ]) \' k2 T    DEFAULT,
0 B8 B; H1 ^3 g2 M' w8 l    NO,6 P7 v" S9 \" M0 }- r. ?
    INTERFACES,
2 M6 Q  U/ o1 w% p    TARGET_CLASS;& ?7 Z! i7 b# z, u! q! R
}
/ O# e7 I' L  t, T  n前面3个,不讲了,直接讲最后一个值是干什么的。. F+ I: {6 F6 X+ w" a
3 b0 N& Z9 L+ K; |+ E8 y: v" v
当@Scope中proxyMode为TARGET_CLASS的时候,会给当前创建的bean通过cglib生成一个代理对象,通过这个代理对象来访问目标bean对象。
+ m6 g& r9 H/ _, h. d7 q! N& D' `6 t3 I# x8 e# \) m) f4 j
理解起来比较晦涩,还是来看代码吧,容易理解一些,来个自定义的Scope案例。! }" S0 K' {$ _' Q$ S
8 R: z* J* E" p6 `" s
自定义一个bean作用域的注解
) U% F4 b$ b: o/ f& D. ?# I# |- D, E- X4 }/ k" h* B* {  Y
package com.javacode2018.lesson002.demo18.test3;
: p# w$ F( y# b. U1 ]9 R/ X1 Z; w: L! j7 @) O
import org.springframework.context.annotation.Scope;
0 J4 C- [% \& ~/ t$ P6 n7 Oimport org.springframework.context.annotation.ScopedProxyMode;
0 V- l9 Q8 t8 j; J  f
( u) ], P( V. F$ a  rimport java.lang.annotation.*;) S/ Q) o1 N5 [9 V: y

9 s3 J# e, Y: b7 T9 q! ?@Target({ElementType.TYPE, ElementType.METHOD})
& M, f" k  b/ k8 m0 q3 i5 q7 S@Retention(RetentionPolicy.RUNTIME)
( j0 m& {4 g7 q4 g@Documented: P; I0 J$ P- V8 ^% E7 r' q
@Scope(BeanMyScope.SCOPE_MY) //@1
5 \  E( q; |% B0 K& Jpublic @interface MyScope {
: C  j0 N" H) K9 k! |    /**6 U8 d2 [: R8 q. E/ H
     * @see Scope#proxyMode()
; Q. \3 Q' V, d9 w+ h) X7 F* \& q     */) s  S4 V) v: u" k  N; n9 P
    ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;//@2
7 {/ \/ n+ @! b$ m8 Q}  z( h2 \$ D  h8 K2 @5 z, [
@1:使用了@Scope注解,value为引用了一个常量,值为my,一会下面可以看到。: o3 r8 q& G0 a9 _
: u% |, k- O" @2 U0 g8 S( {5 g
@2:注意这个地方,参数名称也是proxyMode,类型也是ScopedProxyMode,而@Scope注解中有个和这个同样类型的参数,spring容器解析的时候,会将这个参数的值赋给@MyScope注解上面的@Scope注解的proxyMode参数,所以此处我们设置proxyMode值,最后的效果就是直接改变了@Scope中proxyMode参数的值。此处默认值取的是ScopedProxyMode.TARGET_CLASS5 U  b" J" o. f0 `

1 o+ M& p1 Q( k: `0 E@MyScope注解对应的Scope实现如下
2 K+ `0 f5 e: Q; P
1 ~5 v  v0 l4 U# S' m' Ipackage com.javacode2018.lesson002.demo18.test3;; p4 U$ F% x/ T: S5 H6 ~, ?
8 u  `8 `8 g6 h" w& F
import org.springframework.beans.factory.ObjectFactory;
5 p2 y( o0 j5 j7 ^import org.springframework.beans.factory.config.Scope;
0 \7 N% c& \! t* d* O" F6 eimport org.springframework.lang.Nullable;
7 p8 g1 P* d# b# D% G! M  n& @
  _( V9 y. L  E9 i/**
  V  J& J: m0 d9 m5 [0 _8 d * @see MyScope 作用域的实现
# w- h8 X* P. C9 }" g */
5 R5 }0 C- D  b% Cpublic class BeanMyScope implements Scope {0 r2 f2 b" c2 X' n; |& L9 d
# U" n$ q0 d  b- V' x- @
    public static final String SCOPE_MY = "my"; //@1
3 w  p6 ]( K- O" I) U6 A5 D: o& u2 E; F, G( o3 z+ [
    @Override, e8 M+ ~& ?5 Q. o3 I
    public Object get(String name, ObjectFactory<?> objectFactory) {
2 E5 C8 \, Q* I2 J4 ]# g# H& F( m        System.out.println("BeanMyScope >>>>>>>>> get:" + name); //@2, B( }% y( k6 _4 m6 @* i$ A
        return objectFactory.getObject(); //@3
* p# h* }3 v8 }5 W+ `# H9 A0 p1 D    }
* X* b  J0 ~9 ?3 [9 @/ q" F+ ?0 z5 h4 q2 R5 s& S
    @Nullable+ U6 v+ I5 G. H( B0 ~
    @Override
) R1 `7 `/ O% Q    public Object remove(String name) {. A( ~, k% q1 P$ B2 V" {3 Q
        return null;
+ E, y, s' @' T3 s; P) p8 X    }" k9 |3 d" e3 [  A

, V  _7 R- m$ E3 h    @Override- P2 g$ E0 Z, A% L* H# e$ t
    public void registerDestructionCallback(String name, Runnable callback) {
7 M9 T2 s4 ^3 ^4 d; }
8 X$ x  b+ m; T# @0 X6 E/ z3 H    }
2 Q# o1 p9 X4 E7 P- ?* X. p; W( i! a4 F0 s; {
    @Nullable
+ I+ u& Q5 Q6 N7 }- S) K    @Override. Q0 G! i6 N; b: J( B! y! G# P) e& ]
    public Object resolveContextualObject(String key) {; F9 {; q3 p* o# D) k  `
        return null;& e1 a' \7 p, A
    }  {1 |7 K4 B0 w' l' {

1 P, w* [: v: Y+ B    @Nullable
3 Z3 ^& q  k* c$ B# T8 F3 z    @Override, b( k  Q3 [9 ?3 b7 v
    public String getConversationId() {; l; u) h2 u- e+ c: o
        return null;" T; x$ k. O8 y* Y8 I* _5 L
    }
+ U) O6 F, B4 `7 `$ @( M& I: i}7 R+ K+ C& i& x. @  P. u
@1:定义了一个常量,作为作用域的值
. Y( ?7 c' R% N: @
3 P; Q9 o: W- D@2:这个get方法是关键,自定义作用域会自动调用这个get方法来创建bean对象,这个地方输出了一行日志,为了一会方便看效果+ b' n3 z9 k1 T7 w
% x" r5 b, }2 ]/ k& |) a
@3:通过objectFactory.getObject()获取bean实例返回。
3 X& ]1 T- P: G* c" a4 m% x. p' ^4 Q$ c: Z
下面来创建个类,作用域为上面自定义的作用域  S" n4 ]$ P, `
  B  `( F& q& D* p3 M
package com.javacode2018.lesson002.demo18.test3;; |3 k) n- i! q& S
% t$ C; a! I& i9 j& g
import org.springframework.stereotype.Component;
6 @; {# ^2 D- ]5 }- c( a! ~8 F
2 D9 p0 y% r1 zimport java.util.UUID;
' F9 `3 X, F! k9 V0 A# q/ M5 U; r6 O# L1 P2 D7 A. O
@Component+ s1 x8 @8 C) g% j+ k" r/ b6 z. s$ _
@MyScope //@1
; t& s- U/ W! a5 ~) gpublic class User {8 y0 b% f% d; H+ J8 A: `
% ]. }& L0 j6 }/ b* u: u1 u
    private String username;
- K$ U8 @2 Z) t0 N
/ F6 b  x$ i; ^. L1 r: w) D    public User() {
  b. d1 H8 u. f. \+ ?4 W. [- i3 n' r        System.out.println("---------创建User对象" + this); //@2( w6 R0 Q3 F- _6 f3 H) {
        this.username = UUID.randomUUID().toString(); //@3& l3 C- t. B+ X3 U$ L
    }
1 k# O* u) `$ s( Z9 D) @# F" ]1 t
    public String getUsername() {
9 v* a& C/ K. M$ B* T) R. A        return username;
6 \" @, O9 R. V, b1 Z    }* e& q8 r; M3 t; H) o9 E

# C+ R) N: C# B( K  ~- J% x$ }    public void setUsername(String username) {
, ?, |% I1 e5 O7 [) @        this.username = username;" P* W; H* ~* p; Y
    }/ u+ F% @  Q4 x1 m$ R. V
7 Y# Y* O3 K) p+ R8 q/ m
}
. ^1 N# L0 @7 h- J# u@1:使用了自定义的作用域@MyScope
, e) ]2 T; p, W4 c: c) Z" M$ {* w9 r0 b. z$ C% A
@2:构造函数中输出一行日志
: h  F  Q+ ^4 J. r: h7 r+ e. ^) O* V! w! Z/ w) r. s
@3:给username赋值,通过uuid随机生成了一个
9 E% z0 \( N8 T* L$ v/ b- i0 d5 `) X. d4 j8 r3 d+ \
来个spring配置类,加载上面@Compontent标注的组件7 U6 S! [" N( ^9 T6 o5 f4 d1 g1 k

8 i5 ?, j. W1 {- o+ }3 p6 Spackage com.javacode2018.lesson002.demo18.test3;
( |8 }- e$ R( ?3 u7 T8 R0 b( \% }# O2 j3 k
import org.springframework.context.annotation.ComponentScan;
: _. ]- ?, D2 |* R, eimport org.springframework.context.annotation.Configuration;$ Q' c- B0 @" f1 v; x0 ~
) f6 P9 f. ^0 I: }  W8 P6 o% w7 E
@ComponentScan, `5 Y( v( T$ B( K7 Z
@Configuration. B9 {) g8 _: ~; ^
public class MainConfig3 {8 l  d! i0 l3 R8 Y' `) B
}- _, A  A4 j9 G3 W; O
下面重点来了,测试用例
2 y4 u, i- k5 f2 {$ ?" n2 J  d
2 ?" x0 \3 G& b% E; H: {$ _3 B@Test
9 b# z7 t) U$ @8 Y7 `- }public void test3() throws InterruptedException {
3 X/ \) `! p+ k+ p5 ~3 E6 \    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();( o* m( a, Q! ^- M# r7 s4 v' `/ U# i
    //将自定义作用域注册到spring容器中& C8 O  b+ h; o5 G4 C
    context.getBeanFactory().registerScope(BeanMyScope.SCOPE_MY, new BeanMyScope());//@1& J6 _* ?, n. |/ W9 y4 u; }
    context.register(MainConfig3.class);! ^7 Z% N; l) o  r& q& N: N! M
    context.refresh();
* w4 L* y# ^. w4 g/ B, k4 b; c, {+ [( C' |# |* I6 j
    System.out.println("从容器中获取User对象");% ~4 Y: i) n, l9 n# j* C
    User user = context.getBean(User.class); //@2  [8 H4 b* c. a
    System.out.println("user对象的class为:" + user.getClass()); //@3/ j% I/ m9 Y" j# n5 p) G; q+ m6 T

% Q; |9 f+ K3 K+ R    System.out.println("多次调用user的getUsername感受一下效果\n");3 j5 i+ W' P+ l  Q$ S
    for (int i = 1; i <= 3; i++) {$ B" @0 Y8 h, s. K5 k; m
        System.out.println(String.format("********\n第%d次开始调用getUsername", i));8 l6 g- T$ F1 t8 o
        System.out.println(user.getUsername());
. W' U  f" ~8 N3 z9 f# t, \        System.out.println(String.format("第%d次调用getUsername结束\n********\n", i));# k" d* k& ]6 E8 `7 m
    }! `3 n1 B9 o# ]& L
}' u3 B3 ]  f) X3 W5 |2 s' `4 y. K$ u
@1:将自定义作用域注册到spring容器中- x* x% f9 i- K, v) p" E. R
$ J9 i' M' d( r$ u! m; s
@2:从容器中获取User对应的bean1 r6 p3 Z  t, z& [5 F

. M4 N: _+ w* o@3:输出这个bean对应的class,一会认真看一下,这个类型是不是User类型的- }2 C1 ?; Z; D* B2 H: g

' N. L$ y1 O0 g5 H* R代码后面又搞了3次循环,调用user的getUsername方法,并且方法前后分别输出了一行日志。9 h7 D0 Z6 c/ V. d

5 v0 C* E6 s) G3 q见证奇迹的时候到了,运行输出6 c* A$ X. o& b: K5 [
" \7 ?/ l+ p3 P$ S8 T
从容器中获取User对象
- S( s; Z( _3 O3 Kuser对象的class为:class com.javacode2018.lesson002.demo18.test3.User$$EnhancerBySpringCGLIB$$80233127& C) {$ S# `1 N
多次调用user的getUsername感受一下效果! ~) x4 `. Z3 ?5 w
9 c# D) H& H1 U4 b2 z
********
  r3 H" [$ k) c# i9 @第1次开始调用getUsername6 Y- s6 U% a' }/ S2 d- I4 h
BeanMyScope >>>>>>>>> get:scopedTarget.user+ |. o( f! z8 }% Y5 D4 c$ ]
---------创建User对象com.javacode2018.lesson002.demo18.test3.User@6a370f4" R" q9 [- p# ^- @$ q9 s+ B
7b41aa80-7569-4072-9d40-ec9bfb92f4381 V1 h- X- m$ s* J$ N
第1次调用getUsername结束  Z8 e7 v& N, Z8 Z2 l' B
********
2 I" Q- [1 g, y
( G+ S* f3 o  E0 A0 m3 {$ T********
7 R1 m4 M: \* ?! G第2次开始调用getUsername1 {8 |+ y3 e6 a/ a6 _% y& N
BeanMyScope >>>>>>>>> get:scopedTarget.user
9 P5 p( M4 H! o) a  D- n---------创建User对象com.javacode2018.lesson002.demo18.test3.User@1613674b
- z2 I# m2 }9 B' @01d67154-95f6-44bb-93ab-05a34abdf51f7 M# j1 t5 I+ O9 o6 x: y. M$ `
第2次调用getUsername结束
4 T3 p) s# J8 s' s# c! a% t2 M********
! L; Y" P! X. f! v3 C' G7 B3 G& W( i& m( X4 U6 m& `& x+ a4 N
********, c5 A2 c( i0 H
第3次开始调用getUsername
8 w9 x- Z9 n9 [BeanMyScope >>>>>>>>> get:scopedTarget.user
6 d3 V8 i( i/ z& J---------创建User对象com.javacode2018.lesson002.demo18.test3.User@27ff5d15
9 w, `/ ]! e! Q! m6 e76d0e86f-8331-4303-aac7-4acce0b258b8
4 d) }( i+ M3 T; ~6 E7 O+ s: ^; e第3次调用getUsername结束
- [+ D. b" C8 ?7 X8 N0 J3 D+ H********
' h( s4 S) O; b* P, Y2 h+ @从输出的前2行可以看出:
& a' t; F' C: w  R7 H; J7 y: j" W% y$ S
调用context.getBean(User.class)从容器中获取bean的时候,此时并没有调用User的构造函数去创建User对象
/ c; O1 B6 J) U0 q: L, o
$ n2 `9 _% W; V: g9 y0 }( M; T" w1 F第二行输出的类型可以看出,getBean返回的user对象是一个cglib代理对象。
# H5 J* g7 |; B8 F& O( ~8 N5 P! E, R; g7 H' ]" `
后面的日志输出可以看出,每次调用user.getUsername方法的时候,内部自动调用了BeanMyScope#get 方法和 User的构造函数。
6 j2 U/ o3 W" \5 H* L: V% ], g& ?. _. [' Q7 J1 V% z! j8 G
通过上面的案例可以看出,当自定义的Scope中proxyMode=ScopedProxyMode.TARGET_CLASS的时候,会给这个bean创建一个代理对象,调用代理对象的任何方法,都会调用这个自定义的作用域实现类(上面的BeanMyScope)中get方法来重新来获取这个bean对象。( L8 s1 S1 H/ v6 g2 I& X
& ~) h% h- g$ \* L$ ^' t" f2 I
动态刷新@Value具体实现
; Q; Q! B* d/ r: M5 B  z% a3 w" m/ m
那么我们可以利用上面讲解的这种特性来实现@Value的动态刷新,可以实现一个自定义的Scope,这个自定义的Scope支持@Value注解自动刷新,需要使用@Value注解自动刷新的类上面可以标注这个自定义的注解,当配置修改的时候,调用这些bean的任意方法的时候,就让spring重启初始化一下这个bean,这个思路就可以实现了,下面我们来写代码。2 A& ^% i" m% \+ S7 y

. N3 j) h4 b& ]$ ?- |' w+ p5 j先来自定义一个Scope:RefreshScope
4 t4 E9 G" j' M2 K
  |* j, E6 a/ S) h- O; L1 o  r/ Wpackage com.javacode2018.lesson002.demo18.test4;8 d+ T2 S0 p( v7 p' \, \+ W
* W4 v* K- z% N& D
import org.springframework.context.annotation.Scope;
$ j* L0 y( D, U% Q6 Mimport org.springframework.context.annotation.ScopedProxyMode;9 k$ M. k7 [6 X8 \7 v3 N, `

2 J( g5 }4 [% M( z  f2 iimport java.lang.annotation.*;6 B7 F5 Y3 A/ p

/ T& r+ x' o6 g3 a/ |@Target({ElementType.TYPE, ElementType.METHOD})
( n* G! M1 Y' M: g! T@Retention(RetentionPolicy.RUNTIME)1 d! Z) @  y3 Q
@Scope(BeanRefreshScope.SCOPE_REFRESH)
* T* t& w+ w) e3 S@Documented( H% y3 s' }! i' [
public @interface RefreshScope {
2 V9 O8 s( o% p    ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS; //@1
6 a( x6 W2 R) _/ i1 R  n}" m' _* E2 x! J
要求标注@RefreshScope注解的类支持动态刷新@Value的配置
: g4 I  H- D& @" y$ g+ t, ?- L4 D+ U! Z  X( {
@1:这个地方是个关键,使用的是ScopedProxyMode.TARGET_CLASS
$ @7 V2 k( T) e8 h9 N% ?; ?+ p8 I+ w9 A: L; \3 v
这个自定义Scope对应的解析类
. J( k& W' d, {$ u5 O* H7 H" h. |% h4 o3 s" D7 p# l3 k% o' h
下面类中有几个无关的方法去掉了,可以忽略( G9 x1 ]+ Z4 ^4 S9 o8 Y: X

" w# Z+ U" q5 T- i3 Apackage com.javacode2018.lesson002.demo18.test4;
+ L. ~, C2 J7 z5 e0 S8 @& C  |( h0 O% h, j- l

5 Y% [1 F1 y7 e# U) l9 Yimport org.springframework.beans.factory.ObjectFactory;# {5 U  K( z/ c" B) m2 ]5 m
import org.springframework.beans.factory.config.Scope;
( [( [9 `' I( ]* r" `% ~7 ?import org.springframework.lang.Nullable;
# ^  |0 Q7 R0 G  z0 D6 K1 f1 r
1 m4 c" ~4 E8 `  m/ B! ~import java.util.concurrent.ConcurrentHashMap;& ]5 B) ^3 x2 n; \

9 R$ b$ `4 f( f9 @) [2 @3 hpublic class BeanRefreshScope implements Scope {
* p" ]9 F' [) ?* q
2 r& c' O2 p- p' g# R8 W7 P    public static final String SCOPE_REFRESH = "refresh";& h8 L  u6 A* ^: n4 L% j% L

+ V; G9 G! ~2 I7 P' i9 H0 P    private static final BeanRefreshScope INSTANCE = new BeanRefreshScope();
( V' ~! p, p; i  J4 H# }% O: V: \7 ^& a: [& `2 d( J& c$ l
    //来个map用来缓存bean) _! u3 b7 w) u: E0 V
    private ConcurrentHashMap<String, Object> beanMap = new ConcurrentHashMap<>(); //@1
6 [8 B' x" R) A/ @/ ^
" H. P" c. k) y+ v: P5 h- Q  Z3 Z    private BeanRefreshScope() {
. x" L( A" ^$ d) \3 L3 h    }
0 B5 J4 C# D) ~7 n: b! l- |
& F( X5 A, w# M    public static BeanRefreshScope getInstance() {
9 D5 E7 Y- _! m9 s; H        return INSTANCE;
' w: E+ C5 G2 b0 \    }5 V" r8 {; r, d+ V  x1 k' k4 t

2 }9 X9 r3 c8 G: `    /**( {  }: g+ W8 T) B
     * 清理当前
$ C* |; G+ O$ d( ?- E     */. d) U5 Y2 ~( g  g# W2 e. W
    public static void clean() {
6 [4 w& @. F+ o& |5 F        INSTANCE.beanMap.clear();
+ r  i: H2 _) K( m7 b& V    }
. z, C2 c' t- L7 B4 V) _  B. V& `/ [0 k
    @Override
  k+ ^6 b: J; B) ^9 j+ }* D    public Object get(String name, ObjectFactory<?> objectFactory) {& [' p8 l8 T6 G
        Object bean = beanMap.get(name);
+ Q4 @. K1 `# V$ g: b- |2 Y0 @        if (bean == null) {
5 l! E0 F8 K4 N6 q" G            bean = objectFactory.getObject();) v0 h/ B* h$ f
            beanMap.put(name, bean);( i: n9 o, R" w8 Q; f
        }7 o) t0 X6 y; G9 m
        return bean;. D4 }) [9 G, H2 u7 J
    }6 [4 f0 c2 b+ f' p: Q/ z! _0 H' }: \

$ v7 E" l4 R* z6 x# }( G. c( E- v0 E! Y}/ b/ S% J  r/ H
上面的get方法会先从beanMap中获取,获取不到会调用objectFactory的getObject让spring创建bean的实例,然后丢到beanMap中
, J, K! s* k' x, {5 a) L" o% I
" \0 F5 ?) w% x8 Z( H+ m上面的clean方法用来清理beanMap中当前已缓存的所有bean3 k, O# O( p! C. g- N8 F

6 D9 G' R2 v0 ^. O# C. ~来个邮件配置类,使用@Value注解注入配置,这个bean作用域为自定义的@RefreshScope  z# n& X7 D, \2 `
$ N4 h4 B( }. k$ x
package com.javacode2018.lesson002.demo18.test4;
# J. k; G# W( ^; T; ]9 ^0 G$ q
, M8 n: p% k( f" f3 qimport org.springframework.beans.factory.annotation.Value;9 Q2 a4 q9 o; x1 m: d/ B
import org.springframework.stereotype.Component;5 V0 e! O0 u" U! C. E

5 W$ I' S3 E- o" s! \8 L/**" u; d: K/ s( V& `3 ?
* 邮件配置信息
- }. ]- o' B$ G+ d* z. p. |5 P; p" i */
) C9 t% Z  P& U" z1 O) g- k@Component
: v0 {' L  z, F# g5 D$ E@RefreshScope //@1
. ?- m0 I- ?2 w. ?: ~& I5 O# p/ D" _( D% gpublic class MailConfig {5 `7 z1 l# m1 w3 z. m% ?, H
+ g8 J: {) _/ W$ ~8 G3 d
    @Value("${mail.username}") //@2
9 ^( q6 M! X; }( Y    private String username;( V! h* q) O" ^' _2 f
: r- [7 s7 l# D. O  Q4 W
    public String getUsername() {
& t3 D1 @3 s* h9 \9 P4 y: p8 y8 `        return username;
' T$ |2 F) y( Y0 L    }- U5 c% P; [/ D% D5 U0 X& T

) B0 K% s- J) O    public void setUsername(String username) {
" M& a( M" k) @& @        this.username = username;" o/ j+ Z  d( J5 N6 G* ]
    }& \% [/ d. H4 {/ n4 d

# [- c7 h3 b. ]3 S$ J  c/ |    @Override+ m/ ~3 f$ w6 {7 A
    public String toString() {
' q( z9 d4 h( l( b8 [/ \        return "MailConfig{" +8 s5 m3 V* ^( S& C4 l* i6 P
                "username='" + username + '\'' +9 z5 ?# Y' d( H! R3 x% ?
                '}';7 j. b3 b, Y$ G7 r5 F2 z0 b# `
    }
' j) U" [" n; R" V% l: M# k}& l2 d0 n2 W0 \" ?4 Q- S) s
@1:使用了自定义的作用域@RefreshScope
4 ^7 M/ S) r6 M# o; I1 j$ K' s( s
@2:通过@Value注入mail.username对一个的值) z- M* S8 M5 u! M2 A; F
0 y, {- T4 D5 A. S- B- O- X
重写了toString方法,一会测试时候可以看效果。
  c  j$ y+ _, Q4 |2 A" Q
# x  d* b8 D6 A  ~& F再来个普通的bean,内部会注入MailConfig) l6 P5 E. A. ?% T1 }+ {8 S

; h3 p) E* m# o& c! dpackage com.javacode2018.lesson002.demo18.test4;0 p9 G/ h# g7 C. D
+ H1 l+ \1 @4 o; q/ i# m
import org.springframework.beans.factory.annotation.Autowired;
6 L0 {6 i. y7 `" G$ bimport org.springframework.stereotype.Component;/ f9 l  d& y6 l1 M7 L

( j2 G8 ]& k% G; G@Component
$ F+ D+ w' x" epublic class MailService {
1 T% J6 R* u  D' q* r0 C. Q    @Autowired! v2 G1 v0 X% B0 m3 y& F; ~$ v
    private MailConfig mailConfig;. |. x+ M, |& Z

3 Y! t, T0 |1 ^7 k; v# r    @Override6 z4 {" q: I/ S9 X* u
    public String toString() {' N0 h: L: y& j9 [
        return "MailService{" +
0 k$ P& C& h4 ^0 A                "mailConfig=" + mailConfig +; _: c+ i+ h4 x- _4 `/ a6 j# i
                '}';
  g0 V9 [- X. a: x  Z1 Y; J$ e    }( c) r, Z! o* l# |+ h  a+ J" o
}
' l$ Z/ O  _7 H( T代码比较简单,重写了toString方法,一会测试时候可以看效果。. ?, _" e$ m% a! @, i: e+ ^

: o. V. B8 d5 G4 _" I; X来个类,用来从db中获取邮件配置信息
7 F7 r, {$ T" g5 T
, x6 u3 [2 I/ o. q6 W8 l0 spackage com.javacode2018.lesson002.demo18.test4;0 g3 l8 B; O# |! A5 L

' ]* H0 _" @- i# Pimport java.util.HashMap;& T' m4 s2 e5 i% E' L# U) |
import java.util.Map;; W" u0 \& n5 G* K' e
import java.util.UUID;3 J6 m* M- F* \
2 w& Y8 T: Q! r1 ]. @& ~- L/ J4 W
public class DbUtil {
; Y/ z) m. o2 P    /**/ `% I# d$ f+ a: r
     * 模拟从db中获取邮件配置信息0 z* s% Q: o: b
     *
! W: C9 H- D0 d1 p     * @return! _. J7 A5 o* k
     */
: C' `% w. z1 P    public static Map<String, Object> getMailInfoFromDb() {( ^- k* ?3 `/ f. Y8 \
        Map<String, Object> result = new HashMap<>();
* N# ^' ^( o4 b  D        result.put("mail.username", UUID.randomUUID().toString());
& x$ L* e4 A5 z# a7 j! N        return result;+ ^& i* e( u* u* ~: a( Y
    }7 }! A1 ~- k+ ~" [% B
}
7 f/ x) M/ a8 t1 E& S  `# p* Y来个spring配置类,扫描加载上面的组件
1 G/ m1 c; [- d
* C2 M2 A. U2 Epackage com.javacode2018.lesson002.demo18.test4;
2 p* H! u) N- @6 r5 z% m4 j3 Q! X9 Q% f, O1 D
import org.springframework.context.annotation.ComponentScan;
9 I7 [* a; T4 k6 P! A8 V: |import org.springframework.context.annotation.Configuration;0 g& W  D: T: c' x# W! P+ E
: r( o3 ^; E( W4 @; T
@Configuration  p* |7 v; ~; S" x& B
@ComponentScan
+ r; T; y/ i+ S8 e9 _7 jpublic class MainConfig4 {! N& Y) G7 Z. _/ Z6 q; L! A$ G+ {
}* H+ Q( C' ~/ _2 l; F1 c7 {1 U
来个工具类
; F: y- b1 k) y- b0 y+ {. x8 R% t( B
内部有2个方法,如下:
# p) s% j, l5 g( n! W9 M
7 l5 G. b% O5 O! ~2 R+ J( `$ _2 wpackage com.javacode2018.lesson002.demo18.test4;, W+ A# i, h" E$ l
+ J! e; I1 A/ m% T
import org.springframework.context.support.AbstractApplicationContext;
& h! R% e9 k6 f$ l( U+ M. {import org.springframework.core.env.MapPropertySource;
* v. j( f+ b4 [0 j
+ k4 d9 D: g  f  I% N  f8 _1 x* eimport java.util.Map;
6 B+ c4 j# N+ m* ^  C% R8 i# W
8 a) g3 o4 h: r& I. Bpublic class RefreshConfigUtil {
6 |0 \& H# d8 H! w9 I# f5 G" d( b9 F8 F    /**
. x1 I1 i: i8 X: Z  k     * 模拟改变数据库中都配置信息3 w9 E+ v; O. d1 {* p1 s
     */
/ o* u! l& m) g" r% R4 J6 g3 O    public static void updateDbConfig(AbstractApplicationContext context) {( G- a. G" ^5 S* F# t: P
        //更新context中的mailPropertySource配置信息; V  j4 @  ]& |: `# y
        refreshMailPropertySource(context);
4 d& D4 Y8 L* S1 N0 k: ?) d
$ j1 a: `2 `; x4 |6 T5 U7 y        //清空BeanRefreshScope中所有bean的缓存: ^9 X' q% P9 [7 M: H: a2 I
        BeanRefreshScope.getInstance().clean();
7 S2 p* w1 E0 y, G2 a! H% w1 v1 o+ L6 A    }
% b4 b3 M. Y4 e0 `& j
7 X1 ?# h! ^' k8 f$ l' ?# R2 g2 o2 t    public static void refreshMailPropertySource(AbstractApplicationContext context) {
$ Z7 ~4 E6 T  I9 b3 B+ H: l" b        Map<String, Object> mailInfoFromDb = DbUtil.getMailInfoFromDb();
& d- y; e' d( i2 Y        //将其丢在MapPropertySource中(MapPropertySource类是spring提供的一个类,是PropertySource的子类)+ A6 v) R3 A  Z- k+ @6 [
        MapPropertySource mailPropertySource = new MapPropertySource("mail", mailInfoFromDb);* d5 `# x+ |1 M  P9 C/ G" `: n
        context.getEnvironment().getPropertySources().addFirst(mailPropertySource);4 \; m. x3 \# w. E; S
    }& M* x- }9 l/ g# G4 i8 V# O" a8 B* T

7 n" f6 v3 b( _/ K4 N' R}
, [' b6 V. t( |/ a: ]$ W# n( bupdateDbConfig方法模拟修改db中配置的时候需要调用的方法,方法中2行代码,第一行代码调用refreshMailPropertySource方法修改容器中邮件的配置信息
2 f) [5 n% q. a/ X; b: l/ L8 w' }: t& p4 y$ b5 r5 M, n
BeanRefreshScope.getInstance().clean()用来清除BeanRefreshScope中所有已经缓存的bean,那么调用bean的任意方法的时候,会重新出发spring容器来创建bean,spring容器重新创建bean的时候,会重新解析@Value的信息,此时容器中的邮件配置信息是新的,所以@Value注入的信息也是新的。9 v( n6 U. O3 R5 `) b) S

; Y3 Y; U) X& ]/ ?/ J0 N来个测试用例' \- v# e) q+ ]$ u! V, k
2 S5 ]2 ?  T; G# J3 s3 J7 m
@Test# L9 ^) V2 v, x: S* B# P
public void test4() throws InterruptedException {
3 D! E- V% r3 t3 o: K    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();% i( G! H2 \, F# ^$ }$ D& i
    context.getBeanFactory().registerScope(BeanRefreshScope.SCOPE_REFRESH, BeanRefreshScope.getInstance());- M1 q) N  z, ?3 b+ l4 ]; O) Q* e+ R$ d" j
    context.register(MainConfig4.class);1 ]5 R3 W6 y/ a7 |' I1 ]5 n
    //刷新mail的配置到Environment
: l( e) V1 B9 X- m3 v! v  h$ z    RefreshConfigUtil.refreshMailPropertySource(context);
% Z; ?: V) i6 n6 Q. U5 v    context.refresh();- D+ \& ?) F7 D/ n3 D" H6 B

) a; _% [( J6 R/ G    MailService mailService = context.getBean(MailService.class);3 D$ t9 J6 G, F  C! O8 W' ~
    System.out.println("配置未更新的情况下,输出3次");: i. f% K6 ^& _
    for (int i = 0; i < 3; i++) { //@1/ S7 \% j  e" X  U# L
        System.out.println(mailService);! [4 X# Y8 a# X2 a5 W
        TimeUnit.MILLISECONDS.sleep(200);7 U1 @. N, f7 v& D' k
    }! B* D: @0 b( X

% t( e) t, `, I    System.out.println("模拟3次更新配置效果");
; g! a' R3 R) p    for (int i = 0; i < 3; i++) { //@2" U* G# i+ \! v; G
        RefreshConfigUtil.updateDbConfig(context); //@3( z1 v1 ?7 V. p9 [* H+ H6 h0 Z
        System.out.println(mailService);8 w5 Q; N2 P% t
        TimeUnit.MILLISECONDS.sleep(200);% ~0 z2 x6 L" X2 V$ e6 G
    }
4 n1 ?+ a% O! g! I! y% S! E}
8 G; n/ J3 w) e@1:循环3次,输出mailService的信息& |* ]+ G+ F9 m1 R$ ]) f
7 R5 Y* ^5 P& G6 b9 ]+ P
@2:循环3次,内部先通过@3来模拟更新db中配置信息,然后在输出mailService信息
* u6 t8 y# ~; d. Q- k" N# O" y. m9 V& e: l
见证奇迹的时刻,来看效果9 O1 V7 s7 ?) M
" u  N) v& r: ?* P
配置未更新的情况下,输出3次
' H' N( e$ K1 e/ SMailService{mailConfig=MailConfig{username='df321543-8ca7-4563-993a-bd64cbf50d53'}}
: s. _- ~# Y4 R6 h: C: ~  G: b/ h8 CMailService{mailConfig=MailConfig{username='df321543-8ca7-4563-993a-bd64cbf50d53'}}2 f+ y' D2 `- I  M4 z
MailService{mailConfig=MailConfig{username='df321543-8ca7-4563-993a-bd64cbf50d53'}}
  y# \$ \+ C; D; Q* a模拟3次更新配置效果
) p- d8 p1 J# w/ p0 l) qMailService{mailConfig=MailConfig{username='6bab8cea-9f4f-497d-a23a-92f15d0d6e34'}}6 Z9 g0 f0 {2 I1 ~
MailService{mailConfig=MailConfig{username='581bf395-f6b8-4b87-84e6-83d3c7342ca2'}}
3 j# F9 ^4 z: `MailService{mailConfig=MailConfig{username='db337f54-20b0-4726-9e55-328530af6999'}}( n3 E1 N2 c; E; f  ~
上面MailService输出了6次,前3次username的值都是一样的,后面3次username的值不一样了,说明修改配置起效了。
/ i4 b" E; h' k  r0 x
8 g5 x6 F: L2 p7 d$ u* f% l2 j4 E" i小结
7 b3 a: L% l! d# |9 A& |, e9 Z$ q7 h. G3 e; u
动态@Value实现的关键是@Scope中proxyMode参数,值为ScopedProxyMode.DEFAULT,会生成一个代理,通过这个代理来实现@Value动态刷新的效果,这个地方是关键。
- j7 n; e$ r& m" b4 R
( V3 s" }. w5 _. P8 W7 F6 a6 @: ]8 A有兴趣的可以去看一下springboot中的@RefreshScope注解源码,和我们上面自定义的@RefreshScope类似,实现原理类似的。
* ~, n& B, H  ?: [7 @  p# @% z3 F% s
总结/ m4 U- q8 O1 e$ T% h, ]0 l

0 \$ i6 u9 L$ C2 D4 w; ~本次面试过程中3个问题,我们都搞定了,希望你也已经掌握了,有问题的欢迎给我留言,交流!
  s9 [8 {! v' R7 t" Q* \# J/ n3 I- m
案例源码7 e  H2 Y& Y; p/ q% U- @  x# I* ~
! S: Q( \! @3 V
https://gitee.com/javacode2018/spring-series
5 c$ z* h8 r/ i1 ]+ l& J: r  C路人甲java所有案例代码以后都会放到这个上面,大家watch一下,可以持续关注动态。
! P* h/ s) U) K6 w) X5 V8 h# l: o. _9 ]0 Q+ x
Spring系列. `; ~8 {) t) s
  r  a& Y5 f, y8 S1 H
Spring系列第1篇:为何要学spring?% d! O/ v5 ^% B4 Z0 S  ^

  g' x. K) T. n: W1 xSpring系列第2篇:控制反转(IoC)与依赖注入(DI)
1 ?& N/ l- q6 l) j6 Q9 w9 v& P. e- ?+ p* N
Spring系列第3篇:Spring容器基本使用及原理4 g# f% m# c9 Z( f0 r
2 ^# o# e' n# t' ~( i0 Q' V
Spring系列第4篇:xml中bean定义详解(-)
8 R( u1 X$ R& \. s8 a7 O1 t+ y- Q% Q% z3 ~* K
Spring系列第5篇:创建bean实例这些方式你们都知道?1 q# U# O9 N& Q9 P- N: f
9 _( R5 o+ k/ G0 x5 }
Spring系列第6篇:玩转bean scope,避免跳坑里!; ]" n4 _5 o, O8 ~5 g8 D% S) X

: J& I% K! X( t( r# J" `$ y: o& ]Spring系列第7篇:依赖注入之手动注入
- F% l' t$ ?+ Z7 z! A, G+ T2 \4 h( Q
Spring系列第8篇:自动注入(autowire)详解,高手在于坚持" l/ [( X% \- ~7 m* q+ y

" t) a# K( a$ v/ O8 wSpring系列第9篇:depend-on到底是干什么的?
9 s+ o, Q; M" v% `% F! v; p5 `4 v6 u8 R
Spring系列第10篇:primary可以解决什么问题?; X! S& S. u. w

6 T$ S3 u+ T/ iSpring系列第11篇:bean中的autowire-candidate又是干什么的?
4 c* F8 k, H/ T$ g- k. h+ p3 j9 N6 k5 g  k
Spring系列第12篇:lazy-init:bean延迟初始化& u2 u6 f5 C" ~$ e( N& a- M
- D) V3 \6 ~! t7 Z* }
Spring系列第13篇:使用继承简化bean配置(abstract & parent)/ m/ f7 z2 V0 A' }7 R. c
- w8 }* R( N: v) l: B  m0 y
Spring系列第14篇:lookup-method和replaced-method比较陌生,怎么玩的?
) K1 d# R$ m; E+ Q. h5 r+ ^: j. e5 \" [- q; M) D
Spring系列第15篇:代理详解(Java动态代理&cglib代理)?3 ?$ Y& D$ {4 r, \0 C2 l4 X* P

( o; b. x  U# t! ZSpring系列第16篇:深入理解java注解及spring对注解的增强(预备知识)$ d) u. V/ ~+ \% o. U0 b6 @' l

( [4 q% J% I4 {! `5 v' ~" OSpring系列第17篇:@Configration和@Bean注解详解(bean批量注册)
6 H' w. ~5 l& U2 `  r$ e# `! c- K( T, A# D: p7 i9 D5 j
Spring系列第18篇:@ComponentScan、@ComponentScans详解(bean批量注册)
) R. b% |3 S$ s* ]: S5 i2 Q
9 g- a3 D% K. ]' U) X7 y5 T" FSpring系列第18篇:@import详解(bean批量注册)
& v  s$ b- o' `8 ]  i) v
; w5 J4 D+ H8 H# L2 P4 gSpring系列第20篇:@Conditional通过条件来控制bean的注册6 {# @  \0 S0 G0 J5 ^; f; _

, ?+ t6 D5 D, p( x$ kSpring系列第21篇:注解实现依赖注入(@Autowired、@Resource、@Primary、@Qulifier)9 d* ?! ^4 r$ d) |$ {! x
. z& d. I9 H0 i
Spring系列第22篇:@Scope、@DependsOn、@ImportResource、@Lazy 详解0 `6 }/ D* m0 P0 X+ N
) u0 _# h; f) U: i4 [) @
Spring系列第23篇:Bean生命周期详解( S8 l4 y$ }) g* I( I; x

% J1 n. ^5 E* Z& `7 zSpring系列第24篇:父子容器详解
) g: t  A7 t# |6 X6 R$ s" t3 h+ E# O1 g9 A) F& I
更多好文章% y" v( m& K# `& p% d$ E* d3 c" r/ ~; b
# w2 L6 S8 o* E) i; S
Java高并发系列(共34篇)5 A. o6 E5 {$ G. R8 Y

& ^6 ?( H( C1 V- wMySql高手系列(共27篇)( O" f& q7 q; W  m: K4 H

$ o2 z& H4 s: L. B+ f/ lMaven高手系列(共10篇)2 a$ {+ Y. P# Y& d" W

6 y2 l) {  }7 m1 W6 O7 b% QMybatis系列(共12篇)) l. q. f  _" i. W; E

! Y' ~4 {/ r* Y# R/ |% f5 ?  E聊聊db和缓存一致性常见的实现方式
- Q& x( F+ U. o( k7 h
2 Q, L' z  h) ]% i; D3 C接口幂等性这么重要,它是什么?怎么实现?
* f2 P7 X% X: M. G- L8 G' ^: a
. {7 z9 }9 N( U  y6 ^: c: L. A泛型,有点难度,会让很多人懵逼,那是因为你没有看这篇文章!
9 N1 h) j, t9 z/ v————————————————
: Z  y  W) p# I% P版权声明:本文为CSDN博主「路人甲Java」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。  \$ o5 a/ g+ o( M& b
原文链接:https://blog.csdn.net/likun557/article/details/105648757
! W5 d* H3 H6 n5 E7 a. U# R5 t7 }, z# h/ V

7 @' K# l  ?% S+ `  i6 C8 s




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