数学建模社区-数学中国

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

作者: 杨利霞    时间: 2020-5-23 11:01
标题: 太狠了,疫情期间面试,一个问题砍了我5000!
太狠了,疫情期间面试,一个问题砍了我5000!9 p+ w( [0 R$ W# X  n6 {
疫情期间找工作确实有点难度,想拿到满意的薪资,确实要点实力啊!
- H  j- i# S4 n3 Z- ^3 W. c; X
  g! t* Z  }3 d5 ]: U面试官:Spring中的@Value用过么,介绍一下" P* ~( x7 j9 X# N! `% {
+ H$ L$ A. s8 C3 O
我:@Value可以标注在字段上面,可以将外部配置文件中的数据,比如可以将数据库的一些配置信息放在配置文件中,然后通过@Value的方式将其注入到bean的一些字段中7 P8 N6 _4 ^: X- \6 L# s7 g  x
4 V9 r3 T+ b  J7 N* z' o5 P2 S
面试官:那就是说@Value的数据来源于配置文件了?- s2 c1 ^) T9 l8 _/ y
+ o+ G% Z5 z' i2 n8 e
我:嗯,我们项目最常用更多就是通过@Value来引用Properties文件中的配置
, Y4 A! H0 h6 Y% @8 u/ X5 R5 y1 c2 H$ `9 |5 D" K$ {
面试官:@Value数据来源还有其他方式么?
( g- J! ?7 N4 [. _; a$ \$ s
+ S" a3 J. N$ h! B& v6 b我:此时我异常开心,刚好问的我都研究过,我说:当然有,可以将配置信息放在db或者其他存储介质中,容器启动的时候,可以将这些信息加载到Environment中,@Value中应用的值最终是通过Environment来解析的,所以只需要扩展一下Environment就可以实现了。( H) g3 a; a8 h4 o, D9 B

4 U' c$ \0 r. j" e面试官:不错嘛,看来你对spring研究的还是可以,是不是喜欢研究spring源码?
( ~1 V6 t' W) z  J  P. S- p7 V! e8 R/ R0 h3 X+ d8 `
我:笑着说,嗯,平时有空的时候确实喜欢捣鼓捣鼓源码,感觉自己对spring了解的还可以,不能算精通,也算是半精通吧( C' Y. R4 r0 @; Z; X. M
9 v' A$ C% u; \- N( i  n7 \
面试官:看着我笑了笑,那@Value的注入的值可以动态刷新么?' j$ P. T- M( u5 t

& |6 O5 C. E; I# y! q; F0 h9 j) O我:应该可以吧,我记得springboot中有个@RefreshScope注解就可以实现你说的这个功能/ [6 j* i& {! d

: ?; L0 R; M! i3 C- m+ b1 i! b面试官:那你可以说一下@RefreshScope是如何实现的么,可以大概介绍一下?( ~! r1 R" T& L- C
- J8 G: H2 m  K1 b+ I1 v1 I
我:嗯。。。这个之前看过一点,不过没有看懂
8 d5 E) a, m  m8 Z9 c: }2 Y: ?8 F+ O0 n
面试官:没关系,你可以回去了再研究一下;你期望工资多少?) E0 E3 H$ c$ ^$ V3 M9 @* M9 S% [

" h9 X" t$ `8 e4 T我:3万吧& l- I8 `' l6 {/ \9 a* g

& F* R2 B* `. O/ h$ D! T面试官:今天的面试还算是可以的,不过如果@RefreshScope能回答上来就更好了,这块是个加分项,不过也确实有点难度,2.5万如何?: M, ~% s2 K' l; r& v( @) u8 \

* ]8 L/ Q/ N! ^7 p我:(心中默默想了想:2.5万,就是一个问题没有回答好,砍了5000,有点狠啊,我要回去再研究研究,3万肯定是没问题的),我说:最低2.9万$ V: T+ d4 e' H- x

4 C& \+ }$ V: {: B, T  @+ W- M面试官:那谢谢你,今天面试就到这里,出门右拐,不送!
3 m$ v$ T( g5 @  T% y& P; [& O9 R- ~, L2 S* R* e
我有个好习惯,每次面试回去之后,都会进行复盘,把没有搞定的问题一定要想办法搞定,这样才不虚。' f- a" `; O$ Y0 x1 L
# c! m- P* S# ?3 e! Z, ?" A# f
这次面试问题如下: O+ N- Q: B3 {# S6 [
2 |! p3 Q1 b# Y6 O/ t9 q( `8 b
@Value的用法, t- N# V& b; l, K. U: Z
8 ?  w% T  p/ d5 G! ]( c
@Value数据来源& \4 _( ^+ o- j. c" N+ y
: W# D) t. K+ ^7 I" r! X
@Value动态刷新的问题
) i& t" F3 U- B7 p* X, R
6 }5 z, M7 c: K& U  `5 h下面我们一个个来整理一下,将这几个问题搞定,助大家在疫情期间面试能够过关斩将,拿高薪。
' d/ y. I; T0 o  I2 R
; X2 q5 \( F7 n, B5 v4 O6 Q5 `@Value的用法4 j) `3 i. o9 n! ~

0 P" S/ f/ J" t4 x, j系统中需要连接db,连接db有很多配置信息。" s; @) l3 C& ]- U" F3 s( B- A7 |6 j
7 F4 A1 Q* r3 ~$ N  a5 N" D, S
系统中需要发送邮件,发送邮件需要配置邮件服务器的信息。$ V1 `0 }2 N; }1 C; L! L
3 t9 j- w  J' n9 s! t
还有其他的一些配置信息。' I% V  T) W( a+ y
" ~$ D  L2 H: c
我们可以将这些配置信息统一放在一个配置文件中,上线的时候由运维统一修改。
  a2 S6 [2 J5 e+ @6 H) w5 P
1 }0 ~* O% h% b: t& ~4 Y那么系统中如何使用这些配置信息呢,spring中提供了@Value注解来解决这个问题。; @8 x( }! L" \( f/ J3 C  f
/ Y' ~8 V. _  X. v! y6 e: f' s4 _
通常我们会将配置信息以key=value的形式存储在properties配置文件中。
) W0 j% x- `" j  t* X* \7 _0 L2 X0 {% B( W
通过@Value("${配置文件中的key}")来引用指定的key对应的value。
* r& k  v3 A. y4 `0 Q& a  D( r5 {  E# c, H$ i* D& F' z5 A
@Value使用步骤; F3 N# P# n3 f# P2 }
- z, b& t. o. h# P7 [2 O; I& p
步骤一:使用@PropertySource注解引入配置文件
3 \/ Z# W+ m, P& |2 V( j2 s8 I( n/ u8 i' v" f
将@PropertySource放在类上面,如下0 K% R3 Y  c3 T4 b
: a7 `+ o1 C2 m  l  M+ L: Y
@PropertySource({"配置文件路径1","配置文件路径2"...})% V0 P, \, |( r% e
@PropertySource注解有个value属性,字符串数组类型,可以用来指定多个配置文件的路径。* D4 a/ U4 z; d7 N
$ a3 e: l4 f- F' I  p$ s
如:
  `% d( k; A) z# F  f" D
7 G8 [2 d$ {5 _- A% a5 C@Component
3 N, ?% {- X4 v@PropertySource({"classpath:com/javacode2018/lesson002/demo18/db.properties"})9 ]" K7 }9 f7 c* y/ T% J
public class DbConfig {- {3 C( k: V1 A1 {8 L+ J8 e4 I
}$ G4 e- |- T) F4 I6 h
步骤二:使用@Value注解引用配置文件的值
3 l9 r0 A# }7 P" B
. Y9 r% B+ x4 n0 n6 F. q通过@Value引用上面配置文件中的值:) {5 U; t+ c: u: f9 ?* ~

7 M9 O* T. W; ]& o语法
) Q' b4 x5 O( [2 f3 _) Z3 A
1 [  Z% g( p, R) A' V, P  e1 w3 ?@Value("${配置文件中的key:默认值}")3 _6 ~7 ~( |; M$ [
@Value("${配置文件中的key}")" A  ^( q# ?' }
如:! K5 |( }- _0 N1 g( ]) n

6 v0 j4 V) }- w8 u+ m$ u@Value("${password:123}")
! e) u9 }; }) X7 S/ K上面如果password不存在,将123作为值4 S2 o1 Y3 k0 x1 {+ J* J  l5 _
, b/ \* i( Q6 J+ @
@Value("${password}")
5 R' J. d5 @% z# M; K上面如果password不存在,值为${password}1 e5 d$ N2 b% b8 ~! c; o
8 M& Z+ ^1 I, r" b
假如配置文件如下
- V/ c. _+ B2 @  H) Q' [$ L
9 Q- b" \) m$ {* g1 K: R) L/ njdbc.url=jdbc:mysql://localhost:3306/javacode2018?characterEncoding=UTF-88 G* o2 l" |, e0 h- Q. ]8 q4 W" M* l
jdbc.username=javacode
9 I' G9 \) R6 P, [6 bjdbc.password=javacode
5 s1 i% u1 q9 ?1 r1 r. L8 x使用方式如下:' y- Y9 L$ c0 ]/ ^
% b9 N  a7 E  c5 h8 U, f7 O
@Value("${jdbc.url}")( d6 `8 X0 m8 b3 |& F
private String url;
% E3 }" f6 d3 i2 Z. }4 Z& X* @; B
+ D) c& `, U; p- z@Value("${jdbc.username}")& q8 N7 ?+ }0 j1 K
private String username;
4 \5 M  h6 c7 n+ N  E0 Z) i1 p4 d) w9 m
@Value("${jdbc.password}")
1 h" w! s+ a9 n) qprivate String password;
" B1 W1 u$ s, s+ k4 T( e下面来看案例
& B/ N) G3 ]8 I. _
7 r5 T6 t4 Y9 d& }7 D+ r9 P# Q案例+ B2 H3 l4 j! E# }
" A. n7 m3 P% W$ G) J/ Q5 ], [
来个配置文件db.properties; Y& |7 f% d0 v& t3 w2 b; Y

8 U0 M2 k& ~) p( ?jdbc.url=jdbc:mysql://localhost:3306/javacode2018?characterEncoding=UTF-83 A& \* Y; C4 D9 I& C. _! ^' z
jdbc.username=javacode
  D- s& G& n) f- X% z/ k$ A+ m. {jdbc.password=javacode
3 P3 Q+ O9 K8 t& k! R, A来个配置类,使用@PropertySource引入上面的配置文件+ x& o8 n! w$ x/ j

& W3 B% W% F# I- I4 p8 |package com.javacode2018.lesson002.demo18.test1;
3 B. n; p. `5 Y. H3 u) U% c: A! {3 c. X- p, ?5 r  U( g/ O
import org.springframework.beans.factory.annotation.Configurable;
+ l. ?, Q  w9 Q' ?. H! I( Zimport org.springframework.context.annotation.ComponentScan;/ Q, X9 n, Q0 d3 l7 F
import org.springframework.context.annotation.PropertySource;* M( z9 K9 ]* E  [  Q& [

& D9 G6 c4 A, U; f. C+ v) p@Configurable, c% d" K9 A% B6 f
@ComponentScan
9 a1 r# u$ i4 A; C$ s7 J7 p& G! m% Z@PropertySource({"classpath:com/javacode2018/lesson002/demo18/db.properties"})
9 m" Y: g, X) I" z4 Bpublic class MainConfig1 {
4 n1 p) }9 U: x& b}
7 T8 o: W! Y7 [0 ~. c, @9 E. N0 r来个类,使用@Value来使用配置文件中的信息) y- n( s% ?6 t1 O1 d! j5 L, ~
" f4 E% y, W) }. O: S
package com.javacode2018.lesson002.demo18.test1;
0 f- i$ j+ Y" u  @& D' j0 d6 T! x" l
import org.springframework.beans.factory.annotation.Value;4 ^! _) V3 [- p8 G- y! E/ S
import org.springframework.stereotype.Component;4 r, x$ J4 v: ~( c0 d

" Z  J$ V$ ~1 d! U8 ^: v$ x@Component
7 t* E+ _3 M% wpublic class DbConfig {
# [6 t$ ^; i! h% T8 E0 B% F6 L2 g. F. A/ h6 ~
    @Value("${jdbc.url}")
9 G+ l$ ~) Q  H; e3 t% ?    private String url;* S& y0 G: J! I0 H$ }

. T: c( V- N3 B% g) S8 u    @Value("${jdbc.username}")3 g+ R% Z9 o; b# s4 t0 j0 V1 w5 f
    private String username;8 D6 Q5 n; C* a
0 @6 ~- S/ X7 W8 s1 i7 H
    @Value("${jdbc.password}")
4 j9 N0 L( d- y0 x2 i    private String password;
: r/ T$ W/ J) e) i4 _( a  g! ^' s, @7 ~9 V9 t+ k
    public String getUrl() {
0 R/ o9 }9 v) j8 X3 r9 {3 r        return url;' d, Q' z9 [" J. H7 ~
    }/ {* V# j% u" I* S+ Z; ~9 ^

0 E; N0 b# z' t: D1 N/ I1 ?6 K    public void setUrl(String url) {. h3 u; r/ @8 s4 s, d
        this.url = url;
% ?6 k# h' l0 ]! q$ W4 c    }  ^& H! J" M' ~& ^+ s

+ m$ i0 U4 \/ q2 ~' a+ v/ \    public String getUsername() {5 f1 X5 o' S0 K$ m
        return username;
1 g( M# U5 H! _3 @; n7 O    }" i+ @! x8 S- z  s5 J

2 Y8 N  H  h9 v$ @    public void setUsername(String username) {* T$ q) V1 O8 z( f! T1 w" X% O& J
        this.username = username;! \/ M  e4 g9 I0 Y
    }3 |) o8 N; q9 \3 F& A) I

, M1 m1 `. n+ j! C# z    public String getPassword() {& V2 `+ L7 ~: G. G
        return password;# ^; A! a; ~. G6 r. P
    }' }  {' D( J1 E: Y* R* ]0 z6 P+ U2 ]
* b  g6 m8 J/ s- Q- a; {" d/ q
    public void setPassword(String password) {; K9 q/ E+ s' ^3 e* c
        this.password = password;' I  c/ ?+ y$ u& Q) x% l' m
    }! f: s) W, }! [9 n7 _
' ~6 Y% T( i4 D- O9 T# C2 ^  c
    @Override# g5 A4 s8 q3 o$ p" l, H) M3 K
    public String toString() {: b8 N$ k6 J" Y
        return "DbConfig{" +0 h8 X; B0 A. l$ P  [: p+ O% }4 z* ]
                "url='" + url + '\'' +8 T2 L8 }; q  W/ e7 ]" Y. k1 k
                ", username='" + username + '\'' +% N9 i& a5 V  j* e0 v7 V7 I/ t& C1 H
                ", password='" + password + '\'' +
" b1 v4 g) x' d2 |                '}';6 c/ |0 |) E; c0 e/ ^
    }6 T& B- ~4 a7 d* d5 g; ~
}+ z1 Q$ K9 L- ]  a4 _
上面重点在于注解@Value注解,注意@Value注解中的
) Y( R: Y/ b" R( T6 o! t; q8 e/ @/ j2 {) R& H9 C
来个测试用例
* @& S. v9 o1 [' `: {
4 U. y- {/ P/ h  Upackage com.javacode2018.lesson002.demo18;
% t+ {2 C# P. ^2 C  p5 S: w: S0 _8 }" T' E# V" j; N0 P) G
import com.javacode2018.lesson002.demo18.test1.DbConfig;  s; o* L# _! L$ a5 j1 Y- N  x
import com.javacode2018.lesson002.demo18.test1.MainConfig1;
- s8 M/ r6 h# Mimport org.junit.Test;
4 O+ z: X8 ~/ r3 l9 V' cimport org.springframework.context.annotation.AnnotationConfigApplicationContext;
, H7 S, \. G' O* C4 x5 s3 U/ ]  T6 ?7 f+ [" f7 N3 S, M4 ?
public class ValueTest {
6 E  q, @. W# {; ]7 F: g3 q  l* V% z! R8 V7 h+ V1 i$ d
    @Test
# D  Q# l$ m% i- {    public void test1() {
4 U4 P# i- s& b& ^1 J        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
4 t% k+ ]% O( _! W        context.register(MainConfig1.class);
5 e- X6 A2 o$ T. R        context.refresh();9 c+ e* m: F/ ]. ?+ w9 y

; M8 d# G9 ]$ [5 J/ J& G7 u        DbConfig dbConfig = context.getBean(DbConfig.class);
( y6 z  r0 Q- }( D! q        System.out.println(dbConfig);. t, F) q  I* e
    }
/ w4 d4 q/ d3 X5 ~}6 w9 [* T. p$ B+ M7 |
运行输出# K* @& J& H( D2 V* c

! W+ U: W) ?5 wDbConfig{url='jdbc:mysql://localhost:3306/javacode2018?characterEncoding=UTF-8', username='javacode', password='javacode'}
, {2 T4 X' o4 |4 y上面用起来比较简单,很多用过的人看一眼就懂了,这也是第一个问题,多数人都是ok的,下面来看@Value中数据来源除了配置文件的方式,是否还有其他方式。
5 t8 y) H7 w  G( l
( Y% `& J1 d+ e# d* z@Value数据来源
9 d4 T  k7 Q6 N( n1 z8 b. a
2 `. |% \2 t% T: z& `7 z0 w; W通常情况下我们@Value的数据来源于配置文件,不过,还可以用其他方式,比如我们可以将配置文件的内容放在数据库,这样修改起来更容易一些。
) u) k# W0 j9 D! C2 ]2 R3 @8 D& n2 F0 \" u' k- [& h( j, ?3 m5 B
我们需要先了解一下@Value中数据来源于spring的什么地方。$ _( y8 `  h! K& x; d# }4 f" d) f
2 u& P1 ]# L0 h
spring中有个类
3 m) h$ G) k0 J
' {* K" a4 N( f* Jorg.springframework.core.env.PropertySource5 X: |$ y0 }; v& F
可以将其理解为一个配置源,里面包含了key->value的配置信息,可以通过这个类中提供的方法获取key对应的value信息% o- G1 a9 Y% [7 L

3 ?- B% h/ O+ L内部有个方法:8 Q8 b8 L0 G6 v2 R. P9 q: N
4 v1 t5 O) P+ d1 L5 b7 \
public abstract Object getProperty(String name);
) P/ V2 Z3 v2 T" s* k通过name获取对应的配置信息。
  Z: z  `- }% G5 B, [& W' `
5 L$ x6 n: _7 y* A0 r5 W系统有个比较重要的接口+ I; ?. X1 _; T) x- W

' z# b- v# d/ ^1 J- ], a3 S1 I: u4 Sorg.springframework.core.env.Environment
' T$ V& r/ F/ Z用来表示环境配置信息,这个接口有几个方法比较重要" B9 [* Q. W" p' T6 W$ ~

* q* j- K9 q% \; @3 [7 Y* nString resolvePlaceholders(String text);+ }" `7 ~" N8 J
MutablePropertySources getPropertySources();) X7 |) s) Y6 C% y. Q
resolvePlaceholders用来解析${text}的,@Value注解最后就是调用这个方法来解析的。6 n5 O% t% o; ?6 B% Z: b1 Q5 L

# Y4 @, m0 K5 s# IgetPropertySources返回MutablePropertySources对象,来看一下这个类4 ~. Y2 K( }) V5 i% I, {, T

# g3 k+ g; n" _% l! A" j; {( Vpublic class MutablePropertySources implements PropertySources {/ @+ ]- u/ B3 u6 w7 H
# j" _, d) ?5 @. @
    private final List<PropertySource<?>> propertySourceList = new CopyOnWriteArrayList<>();
" a) z; H& S5 b- S) _" N, c5 b" x# l( @: b2 J
}; q2 E5 }0 O" y( Y& U/ n
内部包含一个propertySourceList列表。
8 H1 e& ?/ C, C  Q4 z0 D) U$ y3 O2 u2 Q+ [
spring容器中会有一个Environment对象,最后会调用这个对象的resolvePlaceholders方法解析@Value。
  u& r$ N# V  i- e5 }% E6 D
* _+ c+ b7 h. j( Q$ U1 }+ x  Q) n大家可以捋一下,最终解析@Value的过程:7 @$ P( b+ ^, w: b% I
2 S: J% l. _7 t8 a
1. 将@Value注解的value参数值作为Environment.resolvePlaceholders方法参数进行解析
# w% z4 o( l8 f/ c( ]+ E5 x2. Environment内部会访问MutablePropertySources来解析% d( T$ a7 b9 d2 l  T  e$ _& `6 c5 e
3. MutablePropertySources内部有多个PropertySource,此时会遍历PropertySource列表,调用PropertySource.getProperty方法来解析key对应的值
, R$ v& u; C4 ^3 c通过上面过程,如果我们想改变@Value数据的来源,只需要将配置信息包装为PropertySource对象,丢到Environment中的MutablePropertySources内部就可以了。/ m1 u1 z# J+ Z

# T* N, G- W+ J/ Z3 l6 q$ e' K- F下面我们就按照这个思路来一个。0 B0 n% ~+ s- [/ k; h
9 B4 ^; O0 Z9 {1 v# n1 L
来个邮件配置信息类,内部使用@Value注入邮件配置信息
0 x4 m+ a7 |0 Q/ B( Y) R% m2 g0 z0 [& |: q
package com.javacode2018.lesson002.demo18.test2;6 F8 i7 \6 }. p( f, e( a

( l; X2 u. c  t" Gimport org.springframework.beans.factory.annotation.Value;" D1 D: E6 a, P  D
import org.springframework.stereotype.Component;/ E; j" u2 {6 A, r7 D4 O
0 Y. M, q% [% o2 P  S9 N1 n
/**
/ x$ b1 a7 v% B' u8 f0 b0 M * 邮件配置信息
7 R; h* k+ C# E# A */- Y( N6 i; R- H, `5 W- \0 J
@Component
1 m7 D  u2 ~: e, ^" }/ @4 y# M+ Wpublic class MailConfig {6 ?7 E  a, l" v- V  q# F! y

# x0 j3 }" }' |4 g0 Z% `$ J2 V! c9 L3 p    @Value("${mail.host}")
' O- L4 @$ N4 b" q9 P- A2 b; k    private String host;1 z1 Y8 Y5 e2 r9 z/ |) A

) x5 S- q, R( b* a! p4 u1 L+ N# ~4 ~  p    @Value("${mail.username}")
+ ?; P5 `- x) t* O. R! f    private String username;; k) |8 w) d5 F5 E

8 J" ]+ C* j1 G1 v    @Value("${mail.password}")
! w# C( F6 F' U    private String password;3 I& r0 n7 p( q7 I
/ A. i# g/ j. G' ^* g6 J- h$ M
    public String getHost() {; c" R. D4 \7 L: I7 W
        return host;
+ C6 C# c0 K9 `! F2 b    }# R+ Z8 f6 J, D" D. F! i1 @  h

8 l# s) B3 v- {; P9 |    public void setHost(String host) {1 g% u% N( d, {
        this.host = host;9 _4 R. d% Q7 @* S1 n- E* j
    }& b# W  N) `, {8 ?1 U- [
2 ^3 c3 Q+ ^7 k! {2 Y2 f
    public String getUsername() {
! J# |5 @2 k6 U. z        return username;
) T) J4 E" E  ?    }
% ^8 I' F. V) m# T2 B  i  d# V2 x* V8 X
    public void setUsername(String username) {$ z: ]2 P; o" z  g, }! U
        this.username = username;
% U! O2 y: ^; w7 S8 w    }
* _- K  u1 j2 O- t$ z  k9 X5 {+ X0 F
    public String getPassword() {
: |* P2 |+ `* @# q        return password;1 L4 t+ ^4 `- [' _* y+ O/ g, @6 ~
    }4 `  X$ ]' y8 b& j, d  X! M

/ h5 _9 Q$ M& Z% t8 V    public void setPassword(String password) {1 ^7 f4 d* M0 S* u6 @
        this.password = password;, D8 V9 ^2 G* a" e
    }: Z4 U- l7 `, Q8 l! q: a
  H- `/ [$ x& ^! O
    @Override
) ?9 M. t3 H/ D8 v0 v' |0 h    public String toString() {* a) M! w+ [4 p8 }6 l
        return "MailConfig{" +& ?8 C  A$ U" t& K' k* x# l
                "host='" + host + '\'' +& I) R  O* h/ U4 h3 c& a3 E. Z
                ", username='" + username + '\'' +
1 q6 a3 Q7 y7 y* @                ", password='" + password + '\'' +
$ m2 u0 V* ^' g; Z4 ~( S% J                '}';
! I0 K+ X5 E: n% E* O3 D( n; V1 Q9 j    }: X8 w# x! O6 q6 X" L
}8 u; w" a3 K, n5 A; g  V
再来个类DbUtil,getMailInfoFromDb方法模拟从db中获取邮件配置信息,存放在map中
0 Q# h' Q8 @0 M5 N3 F; z8 g1 D& ?7 l8 X+ ^4 i6 v
package com.javacode2018.lesson002.demo18.test2;2 D" m, ?, g$ I2 E' @- `

$ n  I. t9 p3 ^8 R% }* h( t" q6 timport java.util.HashMap;
/ s$ E' ?" j- W7 {. I, iimport java.util.Map;! q: K& _, G$ n! [* w
6 k  S& V% ~3 h2 e  G
public class DbUtil {1 j) b% |" f# B
    /**, d+ A2 {2 b8 H2 X0 k
     * 模拟从db中获取邮件配置信息
3 f5 ]9 @% z; M0 O) Z  ~     *
5 d0 E( s( a$ M     * @return
3 }, c$ n  q7 z% L! j- v     */
' u( J$ w, B5 q; b- n7 J% d    public static Map<String, Object> getMailInfoFromDb() {+ n% W* e9 k; ?* `: f) X
        Map<String, Object> result = new HashMap<>();
$ h6 ?4 d5 B! w, ^9 ~& `        result.put("mail.host", "smtp.qq.com");
8 x7 U2 G+ X) s' @6 u        result.put("mail.username", "路人");- n- `- C- U. j5 M- }
        result.put("mail.password", "123");1 l% J- v" L0 j" t0 K6 t
        return result;) X3 `* O# M$ g
    }
2 B* d' X3 R+ a* k- F}) Y7 q& x; E* _& D4 \/ v
来个spring配置类
: k. }. _, z9 S# D: E( j2 [0 F7 X  i7 `4 k+ C
package com.javacode2018.lesson002.demo18.test2;
: [5 V$ ^! b* @4 \, f
2 A# E8 ]5 X; Q5 W5 F' E/ eimport org.springframework.context.annotation.ComponentScan;
% I' i: e1 Z, O) J: M9 Yimport org.springframework.context.annotation.Configuration;
& w% N) C. e4 m) ~0 `" ^  J, f0 P8 @
@Configuration" H% k- }9 C: v
@ComponentScan
9 D5 t1 Q/ \# `0 Cpublic class MainConfig2 {. O8 A' H7 h% l+ T( u3 \. ^6 Q7 C
}" R3 N8 }0 }8 W0 d8 F
下面是重点代码
( I. m/ l6 K/ f3 O% ?+ m2 ]& t% _" ]$ V' v, Z/ m' P* k
@Test
1 j9 @2 u3 P! L' [6 ?% U- ]& z1 dpublic void test2() {
; i' @5 x. K. c" v# T& k# i& M    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
# w: r$ v7 ^" h1 N+ d" f5 q: h! j( B% O4 g% H. K* @
    /*下面这段是关键 start*/" ~5 z1 p3 A1 w' X2 B
    //模拟从db中获取配置信息" A, d3 k6 q7 b$ t* H4 ^' E
    Map<String, Object> mailInfoFromDb = DbUtil.getMailInfoFromDb();9 p' q! x( `! E# X5 n
    //将其丢在MapPropertySource中(MapPropertySource类是spring提供的一个类,是PropertySource的子类)
1 T( |0 n# E$ m0 w& W    MapPropertySource mailPropertySource = new MapPropertySource("mail", mailInfoFromDb);
9 m3 W3 n. s. L' c. N    //将mailPropertySource丢在Environment中的PropertySource列表的第一个中,让优先级最高5 U& H% `: Y( R2 ]7 C
    context.getEnvironment().getPropertySources().addFirst(mailPropertySource);# m1 u- |, {% |( W7 B" m
    /*上面这段是关键 end*// a8 S# d( b1 d) a
& B- X4 {7 |! H6 H( f1 \/ h
    context.register(MainConfig2.class);
$ A" m$ F7 |  t1 O    context.refresh();
+ U7 Y- h% Y* T    MailConfig mailConfig = context.getBean(MailConfig.class);: W3 V$ _$ Z* I7 Z1 z; m
    System.out.println(mailConfig);
, j1 e7 F2 v9 M* x  Y0 m}
" L7 G3 |  q' {2 R注释比较详细,就不详细解释了。
* ]8 u" _$ v1 S; |0 E
& y5 `4 k9 g3 }/ e9 I' ]" s直接运行,看效果
8 z# O3 t& i' ]; O
: c" p! g4 l4 f( h1 X9 J/ XMailConfig{host='smtp.qq.com', username='路人', password='123'}
' a: L( Q& x6 q+ l7 I9 g有没有感觉很爽,此时你们可以随意修改DbUtil.getMailInfoFromDb,具体数据是从db中来,来时从redis或者其他介质中来,任由大家发挥。0 K. `, L6 ?( M
7 X* |, ], A$ d/ z7 c2 |
上面重点是下面这段代码,大家需要理解
) L, ]2 P) L- p. S! z) ^2 v' ^9 {, o1 r' V5 Y7 C- e
/*下面这段是关键 start*/
; K$ p3 }* M: {1 c5 Q  m$ m//模拟从db中获取配置信息7 s2 Z( E+ F- h! `( @
Map<String, Object> mailInfoFromDb = DbUtil.getMailInfoFromDb();4 O# n. |7 w' ^& T
//将其丢在MapPropertySource中(MapPropertySource类是spring提供的一个类,是PropertySource的子类)
& w4 u# Q+ N# r; Q5 e% bMapPropertySource mailPropertySource = new MapPropertySource("mail", mailInfoFromDb);
/ Y9 H9 a  A" s6 X% T//将mailPropertySource丢在Environment中的PropertySource列表的第一个中,让优先级最高) {+ ^1 O! l4 R9 a% y0 M
context.getEnvironment().getPropertySources().addFirst(mailPropertySource);
0 c- H: |/ ^8 W! E% e  [8 X$ w/ q/*上面这段是关键 end*/
$ d* D: a" k: H$ F咱们继续看下一个问题
" i  @/ I& |, h  {" L2 N  }$ A# l5 v& L+ K( ?" R
如果我们将配置信息放在db中,可能我们会通过一个界面来修改这些配置信息,然后保存之后,希望系统在不重启的情况下,让这些值在spring容器中立即生效。! a& F, A" B" N

. `& M0 d* d, p% m' }/ w" M@Value动态刷新的问题的问题,springboot中使用@RefreshScope实现了。
4 d7 o) }9 O/ e" R, }+ `
/ a3 J9 P8 D. [. o" B实现@Value动态刷新7 w6 w" ?6 T  }- u; L
9 r/ q, a3 n' r% x, ?, H/ k. `/ ]
先了解一个知识点
7 ^2 F; Q* g0 ~7 c0 t
2 I6 y! s! Z! W4 k  I这块需要先讲一个知识点,用到的不是太多,所以很多人估计不太了解,但是非常重要的一个点,我们来看一下。
) j. j% G" k% V% S! |# F+ U
- }) D1 B9 H( B  E9 `: L7 H& ^9 M这个知识点是自定义bean作用域,对这块不了解的先看一下这篇文章:bean作用域详解$ p! Q' r, r4 d

: J/ K' X* q0 w9 u: [bean作用域中有个地方没有讲,来看一下@Scope这个注解的源码,有个参数是:( {. j% |% g0 ^5 u& \$ \
  Y- v) q9 H7 Q, U
ScopedProxyMode proxyMode() default ScopedProxyMode.DEFAULT;+ Z2 R. ^1 Z# ~9 D1 C% t  F3 w
这个参数的值是个ScopedProxyMode类型的枚举,值有下面4中
$ Q8 O0 v3 U, Z# Z4 `6 S9 z  m
8 R7 u& h) r6 wpublic enum ScopedProxyMode {4 r% _$ ^, O- [8 Q9 \3 B
    DEFAULT,+ O/ |4 A* b4 {1 X! U
    NO,
. F$ N1 ?* P! Q* M, v* V/ ^    INTERFACES,
6 S' y+ r2 |: r) G  e    TARGET_CLASS;
" N- g3 m. N9 z}2 G: H! d6 \6 a7 L$ Z; @7 o
前面3个,不讲了,直接讲最后一个值是干什么的。
6 i( I0 _  J3 I
; e2 S( j. F' l" P; Y, o  E当@Scope中proxyMode为TARGET_CLASS的时候,会给当前创建的bean通过cglib生成一个代理对象,通过这个代理对象来访问目标bean对象。
, Q- v  f$ j8 P/ P4 ~+ w  H
( K+ ]% K7 N4 q7 A理解起来比较晦涩,还是来看代码吧,容易理解一些,来个自定义的Scope案例。  I2 n( C7 `" L: N  |  E
2 S( P# x( X' O* y( b* a
自定义一个bean作用域的注解+ a( f' Z/ Y$ z
/ L" Z5 \) p; ^" P
package com.javacode2018.lesson002.demo18.test3;
" ^* P9 n, W1 m+ _  M2 F% m* Y' d# i) D* ]1 F/ {, X  U
import org.springframework.context.annotation.Scope;2 W' P" n0 z- W* h) s* _
import org.springframework.context.annotation.ScopedProxyMode;
+ N& h! Y( ^9 F5 v" R3 X4 v- j3 b8 Z/ ?: |* w
import java.lang.annotation.*;
6 z  ^* Q8 o4 m1 S8 g' d+ V0 }
@Target({ElementType.TYPE, ElementType.METHOD})* S$ u5 u/ T' a% y$ O
@Retention(RetentionPolicy.RUNTIME)5 G$ k/ g$ W$ o" }' T- G' H
@Documented
. o; k0 {1 L0 a# ^" n@Scope(BeanMyScope.SCOPE_MY) //@18 G" F9 l9 `0 P: c/ e
public @interface MyScope {3 o5 Q- d- b6 |
    /**
/ F! j9 g0 ]9 i& h! Q" m     * @see Scope#proxyMode()
6 X, g" E( v. `: t- L* t     */) R% t6 T) e$ Y5 {' ]+ l
    ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;//@2$ V% j+ y. W& U6 g& e" V4 b% I
}
9 f2 W* o. c/ m; ^6 b: `+ i@1:使用了@Scope注解,value为引用了一个常量,值为my,一会下面可以看到。; V$ T" c+ A1 Y1 k# u
, @& |) B4 A1 e+ o# P3 Z
@2:注意这个地方,参数名称也是proxyMode,类型也是ScopedProxyMode,而@Scope注解中有个和这个同样类型的参数,spring容器解析的时候,会将这个参数的值赋给@MyScope注解上面的@Scope注解的proxyMode参数,所以此处我们设置proxyMode值,最后的效果就是直接改变了@Scope中proxyMode参数的值。此处默认值取的是ScopedProxyMode.TARGET_CLASS
+ H1 I6 J4 m; p. n1 I
; P) A* ?( g. A0 K! x% N6 F@MyScope注解对应的Scope实现如下
% ?  d$ \# ?% L! E) ?, s( I& j: w+ j1 A- X! e6 N
package com.javacode2018.lesson002.demo18.test3;, X1 @; n0 W5 _, ?
: Q4 p4 [  W9 V( X2 F! [7 Z% R
import org.springframework.beans.factory.ObjectFactory;
5 J+ T& S  _+ r2 R) s: n* W0 Uimport org.springframework.beans.factory.config.Scope;% v6 W9 p4 L  c/ p
import org.springframework.lang.Nullable;4 M6 Z5 c( R; @- Q( R

( H/ J4 l! @: J& ~/**
' @/ V6 U( _& o! n# R' d * @see MyScope 作用域的实现+ f) [8 \8 ^. ?# N
*/
. f; W- v( D0 t6 `" t+ Lpublic class BeanMyScope implements Scope {: L8 P4 M" o$ a5 x. x3 ~

6 @+ W( T& q: b/ |1 j8 L8 ^" G; O    public static final String SCOPE_MY = "my"; //@1* x0 O8 d& U5 H( H* x9 x
" ~1 H! S. ~8 c- w
    @Override$ D0 z) M, S) O( J) j
    public Object get(String name, ObjectFactory<?> objectFactory) {
, g+ D5 U; V! X5 {' h) T        System.out.println("BeanMyScope >>>>>>>>> get:" + name); //@2
/ I& o7 L" d! I9 w        return objectFactory.getObject(); //@3' d3 [* n9 ?5 G! Y+ q0 o* B2 O
    }, Z  A& x, N( _' {
6 @0 t% `$ d% v/ n" [, g
    @Nullable0 C5 @5 [5 r7 A& F% n7 x
    @Override) F/ X& A) a5 `0 z" f7 X
    public Object remove(String name) {
) T- j/ w- X3 T! k. L        return null;
7 h: y4 P& i) _2 ~% u3 C7 ^    }# v% O* f5 n% {- b
3 ~' a8 @5 s4 Y9 G* T  F
    @Override
$ m% E4 C. C7 }( ^+ J    public void registerDestructionCallback(String name, Runnable callback) {) f1 ]6 q% h1 A$ C  r/ g! H% M6 t# z

6 X: _+ k. {# T- l3 D    }
( l# D0 t+ c" y1 f( Y; S# U9 T$ T8 |# A$ |5 l- \
    @Nullable
- }0 [% M5 [- W    @Override
+ p4 K; x0 h. {6 U; S    public Object resolveContextualObject(String key) {
) n: j( f) L8 p7 e0 U3 V        return null;
) i! O! d4 L  ^/ ^2 q    }
* U! m& i% P2 H9 \4 K$ [- w& `- T7 x, _2 o
    @Nullable- F  n7 K* ?$ l; r* [; H* O
    @Override
/ H; `) I$ x! j    public String getConversationId() {* G" |! \  T# }8 `
        return null;& ~% ^- V6 \' B0 @2 H3 I. W
    }
; r" N  b& m4 J7 T3 k" }& ~) r4 G}  E$ X" Q) S1 Z0 ^
@1:定义了一个常量,作为作用域的值
0 @3 t  M+ ~; V6 s1 G+ k2 P- k* u8 j$ b4 U# x0 r& U6 x
@2:这个get方法是关键,自定义作用域会自动调用这个get方法来创建bean对象,这个地方输出了一行日志,为了一会方便看效果
% \  b: \" M* ?' B& s$ Z: x( ~" p8 a, Z7 `3 X! `. Q
@3:通过objectFactory.getObject()获取bean实例返回。. q; l3 @; C2 q4 L, R" s6 P7 v
2 @1 o. }5 c9 ^4 i" S! ^, M. J' n
下面来创建个类,作用域为上面自定义的作用域- f8 Q' {# K2 G  C% m- J

" x/ g9 U3 I0 apackage com.javacode2018.lesson002.demo18.test3;& T" v7 S& ~# F+ Y, P# k6 P

  \& Q5 d4 z3 a' l2 @import org.springframework.stereotype.Component;  J0 I' u- R9 K- b/ W8 Q

# ^+ p( c7 n' X! q/ Rimport java.util.UUID;
9 _- u3 q  p& z& X" v
/ O  _5 j/ z% T8 c@Component8 u, O. X2 x! Z) I6 c" m  g# y
@MyScope //@1 / ?% M' {6 e1 T$ V! `- ^
public class User {, e* w( k  {- R' i
, ~7 a. Y- A6 `# V" T6 p
    private String username;
6 D- _" v/ F8 [. M, C$ p5 S' j: `$ F. v1 ^+ W4 e2 F0 ]) F- h
    public User() {
* ]5 X( \& O& Q* F        System.out.println("---------创建User对象" + this); //@22 z& T. F) T0 h
        this.username = UUID.randomUUID().toString(); //@3
, v; |4 }2 r$ e+ H8 `    }
- L8 L; A; S, K$ L& _. Q
0 j0 M# W# [0 d0 |$ t7 B    public String getUsername() {
+ I$ V. u9 b. W0 h# k4 _. A        return username;4 J, U5 C3 x4 F" d! j0 J
    }6 j8 m# b  o' A$ W* D! }7 c
" v3 x( ^8 e0 ^( L5 H; g
    public void setUsername(String username) {# A8 ?9 h: E# C
        this.username = username;4 e3 j6 ]1 g4 z! a5 v
    }
% V! n' L3 `& f( |0 o" Y- o' H7 n! ^* X& a  b' x+ G( P4 n, f& d
}$ F# |  X3 S& D5 f3 E: s" E5 J
@1:使用了自定义的作用域@MyScope6 }% K; n5 D$ ]) w& m6 d

8 P  p( N( z+ S4 y) F& l( x@2:构造函数中输出一行日志
3 e0 b" t1 L& b5 E  C2 Z
% k2 t) M( ]3 A7 |- \+ ]@3:给username赋值,通过uuid随机生成了一个; D' L+ P( i1 i$ y# @; q! o6 X' b* |& R
! F, Z9 @7 u# V& T3 q
来个spring配置类,加载上面@Compontent标注的组件
7 L- d/ n- Z. C) y" N' g) e- x( K/ b" u
package com.javacode2018.lesson002.demo18.test3;
6 R! R9 o- c- g7 [7 ^2 U
  f) w9 w* l* T/ nimport org.springframework.context.annotation.ComponentScan;  M% S5 N# I4 u% j
import org.springframework.context.annotation.Configuration;3 _( P3 _/ r! F8 K  D

$ u: I# e& j0 N/ b; }' `- [1 g@ComponentScan
/ d4 k" {& J, M@Configuration
  S- l8 K; o  Y/ c+ b7 \+ gpublic class MainConfig3 {
$ h3 y1 a$ p! a" i; B}' z- M/ n* M* J5 _% a- Q" m
下面重点来了,测试用例
; i4 K* u2 X8 }! i, G: g( ?' a( v5 T8 Q3 ~
@Test* q! D& A  j% L1 M" s
public void test3() throws InterruptedException {4 y# |/ |6 B+ J7 h* _5 j8 X
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
" n) n5 A  @* }" h    //将自定义作用域注册到spring容器中+ f3 G7 _, z* X, W' I% }* N! o
    context.getBeanFactory().registerScope(BeanMyScope.SCOPE_MY, new BeanMyScope());//@1
4 v' B) g& y3 h+ [    context.register(MainConfig3.class);4 N2 V$ {9 B) \4 B
    context.refresh();; n  J$ B) l( Y3 q
9 u; Q8 c% u8 D. c9 i
    System.out.println("从容器中获取User对象");; a/ e* Y2 E6 S2 f* W
    User user = context.getBean(User.class); //@2* a# r2 m; B; {  H- |
    System.out.println("user对象的class为:" + user.getClass()); //@3) `1 {8 {+ l/ ~/ Z; P

6 G" E5 `5 g8 w2 ?    System.out.println("多次调用user的getUsername感受一下效果\n");. {: ]4 z* _, Z: v/ t1 M" j
    for (int i = 1; i <= 3; i++) {- A/ ^( X- q% |, k1 g9 ?: |, n
        System.out.println(String.format("********\n第%d次开始调用getUsername", i));
$ P' V& H  D/ }$ A        System.out.println(user.getUsername());
& w5 J2 s% z% P0 J4 B4 j        System.out.println(String.format("第%d次调用getUsername结束\n********\n", i));
9 w( M, i# F  g    }
' x: v- a% y8 Y* q( O$ N/ [- Z+ A}
8 B7 I- L2 ~8 P4 t$ [* o* Z7 P@1:将自定义作用域注册到spring容器中" o( ?+ K6 ?( f; {! @" P
0 R# y( H* ]) W" W
@2:从容器中获取User对应的bean
7 j% D7 _+ p* N, \( U
- Y& s( p$ U8 f@3:输出这个bean对应的class,一会认真看一下,这个类型是不是User类型的3 c+ g: `) ~+ ^$ u& Y, A8 k. R) I0 q" a

( l4 s. C) H+ h( a6 L6 b代码后面又搞了3次循环,调用user的getUsername方法,并且方法前后分别输出了一行日志。( [, a; @: n! P6 j) S" b" k5 E3 {

- R. y" J6 t3 q6 m0 N8 b  J见证奇迹的时候到了,运行输出' r5 B3 Y4 B$ P+ \
, _* S. M' o* j2 S
从容器中获取User对象
- X) z8 C: U# W6 T( N2 w6 @5 auser对象的class为:class com.javacode2018.lesson002.demo18.test3.User$$EnhancerBySpringCGLIB$$80233127
6 \: u. K0 W/ \  \9 n8 d多次调用user的getUsername感受一下效果* t: V' O+ [8 D

' e3 t7 Z0 J7 u/ @0 ~+ e********- P: y' l. W( u0 U2 o
第1次开始调用getUsername
0 X' q  E! b# l' R+ A- O/ C7 P1 E1 B4 d6 C! BBeanMyScope >>>>>>>>> get:scopedTarget.user) c& G/ K/ j& u9 B
---------创建User对象com.javacode2018.lesson002.demo18.test3.User@6a370f4% }! N0 F$ L2 r# O& t2 o0 }
7b41aa80-7569-4072-9d40-ec9bfb92f438
! j' q. K( x, n第1次调用getUsername结束4 J9 N) `- Y! I- q
********
3 Q- j) ~9 J0 X" v$ n* x3 D9 Z1 O( j1 l$ Z( b1 Q
********6 r4 p% Q0 W7 ~! G9 ~; o0 W
第2次开始调用getUsername9 {4 `3 P' j' L# u' d; i+ b
BeanMyScope >>>>>>>>> get:scopedTarget.user
5 Z0 u* v! f9 M! p* A---------创建User对象com.javacode2018.lesson002.demo18.test3.User@1613674b
0 _2 Z: w. ?" l" a01d67154-95f6-44bb-93ab-05a34abdf51f
1 r, F2 u0 U9 q! |( t' G第2次调用getUsername结束
- \4 l$ h  f# E) q& ^- N********# y  N" j) c+ r4 S" U

2 K8 }5 `1 n6 D0 j, h' [0 ]. `4 @********& g9 g6 H: W) J/ V" y
第3次开始调用getUsername
! h/ j% n6 ?% iBeanMyScope >>>>>>>>> get:scopedTarget.user
2 Q, z2 p6 r7 B---------创建User对象com.javacode2018.lesson002.demo18.test3.User@27ff5d153 ^, U: e! k2 U% p$ c
76d0e86f-8331-4303-aac7-4acce0b258b8
& ?% w& b* [# ?/ ^第3次调用getUsername结束& F; }: a0 c) z- h( c
********# y: r6 b/ h0 w( j
从输出的前2行可以看出:
" s  n" w8 }! f# g
3 [0 ]3 G* g: t$ n调用context.getBean(User.class)从容器中获取bean的时候,此时并没有调用User的构造函数去创建User对象# s( O; W! \" W

/ m% g8 z, A+ A! g7 U第二行输出的类型可以看出,getBean返回的user对象是一个cglib代理对象。
, Y. H9 d4 f! l. |3 l- T( N  k: ?+ y# [$ G
后面的日志输出可以看出,每次调用user.getUsername方法的时候,内部自动调用了BeanMyScope#get 方法和 User的构造函数。/ M) G5 j/ o0 m7 L% ?4 X; V# q
+ }1 y& d. _( P* ]
通过上面的案例可以看出,当自定义的Scope中proxyMode=ScopedProxyMode.TARGET_CLASS的时候,会给这个bean创建一个代理对象,调用代理对象的任何方法,都会调用这个自定义的作用域实现类(上面的BeanMyScope)中get方法来重新来获取这个bean对象。
0 M% p# A% i) N$ k8 l" G/ @5 T. P. K/ b8 o
动态刷新@Value具体实现
- d! g% d: @) k; C8 H& R
- L) d- a9 _. W8 `" i- s- b那么我们可以利用上面讲解的这种特性来实现@Value的动态刷新,可以实现一个自定义的Scope,这个自定义的Scope支持@Value注解自动刷新,需要使用@Value注解自动刷新的类上面可以标注这个自定义的注解,当配置修改的时候,调用这些bean的任意方法的时候,就让spring重启初始化一下这个bean,这个思路就可以实现了,下面我们来写代码。! e' W7 i' Q5 t4 W/ a3 K
$ M4 P+ J/ u. {0 M
先来自定义一个Scope:RefreshScope5 u0 w. D7 n7 z) o' Q
+ \  Y% ?5 L- e2 v) B+ l
package com.javacode2018.lesson002.demo18.test4;
# j1 i8 a2 z: ~
. r) U. B) }4 x8 {! u& ]+ Cimport org.springframework.context.annotation.Scope;1 }7 ?) |! V' _- p( b3 [
import org.springframework.context.annotation.ScopedProxyMode;
+ x9 T4 U# H9 t9 D, Q6 n2 w+ H$ R- t$ c7 Q3 Z
import java.lang.annotation.*;( D- e' O( ^" m$ m, X! t
2 Y4 r9 X! E. b- r  @
@Target({ElementType.TYPE, ElementType.METHOD})
0 j5 F8 a( F, q/ ?: s@Retention(RetentionPolicy.RUNTIME)
$ X) ~) j2 [) v; A3 Z7 P@Scope(BeanRefreshScope.SCOPE_REFRESH)
7 C2 g. {$ I$ @, |$ Y@Documented
: ~+ L6 k: [5 {& Zpublic @interface RefreshScope {' @# y$ v' u( p2 P, g% U  T+ j
    ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS; //@10 N* N, G* o& a5 t; m2 |9 d
}2 y- f9 \; a: l$ g
要求标注@RefreshScope注解的类支持动态刷新@Value的配置4 }  W! o6 `& ~, Q" B- g* u8 M1 i6 Z( x

/ F( W1 ]. [3 _9 Q2 ~@1:这个地方是个关键,使用的是ScopedProxyMode.TARGET_CLASS5 t1 U0 g# ~  e% Y& o2 {5 O
8 ?6 s6 `7 H- i0 Q% Y
这个自定义Scope对应的解析类
* g2 I$ O- ^: w9 p; W- \  f  |/ D% u- A% J
下面类中有几个无关的方法去掉了,可以忽略  h; r+ R$ I. t

& i1 R, ?* X- I3 tpackage com.javacode2018.lesson002.demo18.test4;
) P) J- k1 r/ H1 Z- U7 t1 C
; G, N% o4 n/ g6 f
5 }% ~$ g' H- D/ m1 S0 s3 F' _import org.springframework.beans.factory.ObjectFactory;5 q4 T2 o5 @+ H$ S3 a( G9 j! j% ?
import org.springframework.beans.factory.config.Scope;  H/ ~* L" g) y# \5 S1 L0 A9 I
import org.springframework.lang.Nullable;. v& a* B2 u2 {" _: R$ y

; s( L4 z0 t" U+ F2 m6 Vimport java.util.concurrent.ConcurrentHashMap;
" Q  L7 I( B: G" V
2 z: x3 t' R! @" Hpublic class BeanRefreshScope implements Scope {) c/ k; S& C, H2 I8 C2 ^& \

2 g7 l& y5 l* h  a( i    public static final String SCOPE_REFRESH = "refresh";+ f+ }  \0 Q! k9 I  N& ~

0 |8 r$ Q/ D9 P    private static final BeanRefreshScope INSTANCE = new BeanRefreshScope();
: `/ C! Z% b) J3 ^, R' Y, X8 U% K& m+ Q5 }0 ^, d) b
    //来个map用来缓存bean
5 c0 f+ ~  O. V8 H6 O$ t    private ConcurrentHashMap<String, Object> beanMap = new ConcurrentHashMap<>(); //@1
- y  `0 _! J: [9 e* `" Y: l/ s9 b. h; p3 A4 b" o6 N" D* M
    private BeanRefreshScope() {, x2 R+ h% C2 G5 E
    }; t7 o% R0 Q1 U) ^. X

1 p8 i, ~+ H1 k# m    public static BeanRefreshScope getInstance() {7 f! J% X3 b1 |. W3 M5 M6 r
        return INSTANCE;
# ~% s5 s8 ?/ c" e0 j7 q) ?    }+ J1 p5 j0 ?/ w+ F) y! G3 v! X
! n- C& S7 x+ `9 f
    /**
9 C3 Q; \9 V* U# t4 d     * 清理当前
- B% i$ H" V4 f0 O6 x3 k8 I     */
& ]8 F' o" z+ p/ @% q1 x    public static void clean() {
" {2 U+ b9 a; T  T# `        INSTANCE.beanMap.clear();
1 w+ g6 V% Y4 k. G    }2 ?% T7 I/ K7 \- R4 c4 H* m
5 d8 Y+ q: I3 D& t5 u+ u
    @Override7 U, E) Q( }2 d3 q3 O
    public Object get(String name, ObjectFactory<?> objectFactory) {! w1 C" x4 I5 R
        Object bean = beanMap.get(name);
- g$ o- R: ~1 S! n' f9 V; p$ s        if (bean == null) {
3 g, k( e, B3 C) u; X) Y! b) B            bean = objectFactory.getObject();
5 ]+ s' F7 t. {- f# s            beanMap.put(name, bean);1 G9 X, L( H/ Q1 @
        }. Q/ ?; y" v. J4 |, X
        return bean;  R" t2 M) {3 w5 i
    }
4 t5 D& y  I/ L. l* j7 @+ a
/ e8 S& y/ U# g- x/ K9 ~! S0 q6 A}
5 U) z) L' [6 o+ h2 P; S" ~; B上面的get方法会先从beanMap中获取,获取不到会调用objectFactory的getObject让spring创建bean的实例,然后丢到beanMap中
- r8 r: s+ U2 i8 w
* K( i3 p; U8 y2 Q4 p* f上面的clean方法用来清理beanMap中当前已缓存的所有bean! ^# v: g/ V3 y- ?) I
9 X/ h  F. W0 L  Z. z
来个邮件配置类,使用@Value注解注入配置,这个bean作用域为自定义的@RefreshScope1 w% T& L' T' B6 G& K

5 R2 @+ k5 H; s" w0 }package com.javacode2018.lesson002.demo18.test4;
$ ^: P0 U' [: H* `, B9 d- R  {) x, f5 a& ~" o
import org.springframework.beans.factory.annotation.Value;
* D: K, f  H+ |! wimport org.springframework.stereotype.Component;
, o/ z. ?' J6 H" ^3 f
9 v1 U5 d6 P8 w: l6 G6 \# C' x6 a/**
# l2 |# d2 Y2 i$ i * 邮件配置信息
: a1 n: A1 n" b& v8 w8 Y *// y! y7 n1 }! m1 w9 f* C
@Component
; l: m6 Q8 i6 ]* _) O) l; y@RefreshScope //@1
9 b0 k. o; w7 O: N) Z: Dpublic class MailConfig {
" Y7 N9 K. e& u6 n" e1 t9 d
/ G& ^: Z0 s7 X$ Z- k& g) S    @Value("${mail.username}") //@2' E3 g0 a# J# n: B' R
    private String username;' _1 A& @+ d0 o8 I7 F0 ?' f  j. F

, p1 \# o! ^. v2 e5 {    public String getUsername() {' F! s, r- U" P% m, }
        return username;4 g# X$ D3 t: @$ i4 y2 Q
    }: l$ j9 Y( v0 k3 J. Y0 N
% Y/ \: D. S' F( h* L
    public void setUsername(String username) {
5 W5 o1 Z! X8 x0 s% h) K        this.username = username;
4 y) D1 `* E& m6 Z" _) P    }* A: g# a4 t, B% r" ]

  }& O; J- E: w. h' _7 d    @Override
) x( ~" _6 {9 `  ?; [    public String toString() {
: _2 F: d% u( e# m- I; i. f        return "MailConfig{" +
0 \8 q$ Z! \  v! O3 q) ~9 I, y                "username='" + username + '\'' +
1 I8 Q7 {5 R; m- H/ W3 F- I1 i                '}';7 p# b- _# n( [6 ]
    }
  a4 A. B7 P' X" i}
8 q# n' {, F; J' D% y8 \@1:使用了自定义的作用域@RefreshScope
* G2 P% `7 D* F
9 |$ K, I: a. I4 c@2:通过@Value注入mail.username对一个的值
1 w7 d/ D$ e: }* L
' c" K2 A/ Q8 w5 ~0 U重写了toString方法,一会测试时候可以看效果。( \) h$ R2 E/ f# n/ s# h

. F8 i* p! e% @/ r; M再来个普通的bean,内部会注入MailConfig
& s. c- h$ e# u- r6 U# M& u
: {2 V" t( H0 b3 x0 Ipackage com.javacode2018.lesson002.demo18.test4;) K' V$ g& L2 w# F
0 w0 \) v% L( ~0 e  N3 y" i
import org.springframework.beans.factory.annotation.Autowired;  u9 v  a4 H" l" ?8 M) i
import org.springframework.stereotype.Component;
) m* g- ^# F$ i1 y# O: x
' B5 k! w* E) O( B- x@Component: V# @! y5 n( _$ }
public class MailService {- i* Y' Y3 Z- g" t* a
    @Autowired6 I' N5 [) |" K! @8 k3 H
    private MailConfig mailConfig;
! m  z5 K' U+ B# m2 j0 [2 R3 z$ L' [5 M
    @Override' U4 L$ W! p; @
    public String toString() {
( R% w8 H  t+ W- P4 m# k        return "MailService{" +
1 P  T! R2 p2 T0 ^                "mailConfig=" + mailConfig +
1 l# f, n% n+ a8 y7 o3 b                '}';% x8 R' `7 e" j) m# C! t; B
    }
" ]* h$ p; g* {( a  D}$ i4 d: k! P6 g, I
代码比较简单,重写了toString方法,一会测试时候可以看效果。% b, ^  y: ]5 X0 e2 n( H8 Y+ p

6 k6 H8 D# K/ C( J来个类,用来从db中获取邮件配置信息
5 s% a2 J9 T7 C7 r! G9 ?7 m: ]/ s) G2 p% B7 `3 r! g
package com.javacode2018.lesson002.demo18.test4;
% [/ n" E0 W7 t( Z0 ^! U6 g4 P( |9 z, [' F* i0 D0 e
import java.util.HashMap;! V8 a' }7 \1 A4 o1 ^' N1 i+ t9 n
import java.util.Map;
, |" K) M! {: H4 r5 f7 \import java.util.UUID;
6 [6 x  C6 s5 A& t. ^- q9 h6 {4 k4 y3 o. n8 Y
public class DbUtil {
3 A; X6 ?$ z8 f$ ^! x' p    /**
8 E) r! T% i2 A: F     * 模拟从db中获取邮件配置信息! w) ]' t2 v1 ^) _
     *
0 o' u; k- ]$ q: u, r     * @return
- S6 P6 O7 g! s     */. N6 q7 o4 Z6 c  p
    public static Map<String, Object> getMailInfoFromDb() {* r  Z; ~% B1 Q) y7 i0 p6 j+ h5 m) e
        Map<String, Object> result = new HashMap<>();
3 w& O# y, H1 i        result.put("mail.username", UUID.randomUUID().toString());
7 l" _4 z8 A5 W+ t        return result;* d* n) g: u3 u! c+ ~" P$ M
    }
+ u4 l% g7 ?" t: c* \% ~/ N4 z* q}
# {$ F  d- u) ^( ^) p来个spring配置类,扫描加载上面的组件
6 ]) O$ r5 |( m3 C; ]) l! r$ ^  g+ K$ M1 C! t
package com.javacode2018.lesson002.demo18.test4;" C( u: H/ D" E( P
* d( z" H" |! L2 g) \( G" D+ L
import org.springframework.context.annotation.ComponentScan;
# N( A' v+ M, v. J4 R1 Bimport org.springframework.context.annotation.Configuration;
# C+ G* K4 t  P9 e5 ?! j! T9 C: P+ U* Y
@Configuration
5 X- z2 b8 W# ]@ComponentScan
1 t$ e: _+ E' fpublic class MainConfig4 {  X5 C% K& a" T. a9 U9 g' l. N$ d" J
}& I( G! P7 }6 M9 T
来个工具类
# h% l9 @- t8 w4 Q  K0 W9 v, O1 t1 h" C
内部有2个方法,如下:
) V2 [1 {6 r/ V. z+ \4 ]
+ Q* h$ C+ g2 w, U: t/ H2 Xpackage com.javacode2018.lesson002.demo18.test4;
6 X  {* Y' y: J2 Y, |' s/ ^/ F9 Q, t3 p: s- K4 x/ T# Q( g
import org.springframework.context.support.AbstractApplicationContext;! p0 K8 t* o. C0 m" V) f7 P+ o
import org.springframework.core.env.MapPropertySource;9 r# p7 E( w" o, k; M

2 z, S" {* N6 D" @* E! Zimport java.util.Map;
5 p& H- M3 |( ?3 y
. \4 J$ Z* R* U; l) qpublic class RefreshConfigUtil {' b* V: n0 p1 P6 M
    /**9 `# y" y7 J3 S. i" \0 k
     * 模拟改变数据库中都配置信息: U' A% m4 D. a/ |' k8 p
     */
& z9 D) g! m8 O. E5 `    public static void updateDbConfig(AbstractApplicationContext context) {0 |* V! V. o- T+ v6 H: h6 c
        //更新context中的mailPropertySource配置信息
4 Z4 h6 m0 k- V/ I        refreshMailPropertySource(context);
6 R. X4 \2 k1 U5 {# A) ^% U7 m+ P; ~/ r6 n) j3 X
        //清空BeanRefreshScope中所有bean的缓存: r; v; G7 i* M" m& [' B
        BeanRefreshScope.getInstance().clean();
8 {$ H0 g2 y* K* u4 |; e    }3 M( X6 F  L0 S8 C+ o
2 V* d  T9 O. x: D, A) r
    public static void refreshMailPropertySource(AbstractApplicationContext context) {( Q, H8 K3 x8 c! ~" D
        Map<String, Object> mailInfoFromDb = DbUtil.getMailInfoFromDb();. X/ I4 p* D, ^2 j
        //将其丢在MapPropertySource中(MapPropertySource类是spring提供的一个类,是PropertySource的子类)
& ~8 E6 `  Y! ?! p3 N# Q        MapPropertySource mailPropertySource = new MapPropertySource("mail", mailInfoFromDb);
( v2 [5 h( e" K+ {$ T6 t1 c        context.getEnvironment().getPropertySources().addFirst(mailPropertySource);
8 g/ A) V$ M$ h    }
2 |( ?" K# d4 o3 c5 `' r! r2 N7 [1 H' c
}$ B9 M5 M/ [" x" Y; b3 D8 p
updateDbConfig方法模拟修改db中配置的时候需要调用的方法,方法中2行代码,第一行代码调用refreshMailPropertySource方法修改容器中邮件的配置信息
& [2 @* A" n* {7 L
5 R. S4 q6 s  {  x. HBeanRefreshScope.getInstance().clean()用来清除BeanRefreshScope中所有已经缓存的bean,那么调用bean的任意方法的时候,会重新出发spring容器来创建bean,spring容器重新创建bean的时候,会重新解析@Value的信息,此时容器中的邮件配置信息是新的,所以@Value注入的信息也是新的。
+ V0 b4 ]4 [6 L5 T
2 o* x8 S. F4 d$ t4 u* k来个测试用例/ Z; e7 n( k) C( T

3 [* M; B  e1 _1 t" Y$ P@Test
6 G+ l4 {% i/ s# P: r4 R+ ?public void test4() throws InterruptedException {
' o* ^( x' U( Y# J, @+ H    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();1 Z( |6 h( b# Z1 w
    context.getBeanFactory().registerScope(BeanRefreshScope.SCOPE_REFRESH, BeanRefreshScope.getInstance());
; I# P. w% H) P) V+ }; T2 C    context.register(MainConfig4.class);
3 C. b: `& s' R0 x* v+ ~; ?    //刷新mail的配置到Environment9 ^+ R, A+ s1 x9 w9 y) C
    RefreshConfigUtil.refreshMailPropertySource(context);$ N. b3 Y1 K7 }" T' ?* D
    context.refresh();
8 U, {* D2 s6 e% R2 N: i  V7 n0 `6 p/ q
. m% D5 f5 F! t3 O    MailService mailService = context.getBean(MailService.class);
/ C+ c7 e) v, n& K4 ?6 z. [    System.out.println("配置未更新的情况下,输出3次");* O+ P$ ?7 c: M& \% W# {
    for (int i = 0; i < 3; i++) { //@1
0 k5 h: d" r+ C8 ?) u- p. V" v        System.out.println(mailService);, I$ y; a' r" q1 k  F! e. E
        TimeUnit.MILLISECONDS.sleep(200);
' U, {5 Q  C2 ]# N2 O    }
5 P' H3 f7 C" s9 z( a, A5 P& ?
/ f/ O8 t) F" t6 a, ~+ t, _5 R    System.out.println("模拟3次更新配置效果");
5 z' l: j. n6 M' W, t8 M9 V    for (int i = 0; i < 3; i++) { //@2
3 D/ }& z; h% s0 W) g        RefreshConfigUtil.updateDbConfig(context); //@33 K- F8 U+ }8 p" B9 L% [
        System.out.println(mailService);) N  T' P6 J7 J+ p7 L' o9 j! j* o
        TimeUnit.MILLISECONDS.sleep(200);5 U0 P* t# {9 j0 V6 U0 {
    }
( n; S# M7 c% y& Q& V, I0 e}, N& L: d+ K) d' {% x- u. ]
@1:循环3次,输出mailService的信息5 P' L- U/ u8 K- R) A$ T; _

/ ]& B! ]5 S- X8 s3 L9 \# x" j@2:循环3次,内部先通过@3来模拟更新db中配置信息,然后在输出mailService信息
1 C4 v) D: Y8 a" X& E
8 J) O$ K9 a4 e0 j5 l见证奇迹的时刻,来看效果
0 Q; t" g6 n& h8 }& y  n* Q& V3 p7 K4 u
配置未更新的情况下,输出3次, x! k. C% @+ t% L* H4 B
MailService{mailConfig=MailConfig{username='df321543-8ca7-4563-993a-bd64cbf50d53'}}4 H# Y) G% }. P4 `
MailService{mailConfig=MailConfig{username='df321543-8ca7-4563-993a-bd64cbf50d53'}}9 a9 q( M9 w& d6 b: c- T+ ?) b
MailService{mailConfig=MailConfig{username='df321543-8ca7-4563-993a-bd64cbf50d53'}}
! Z! {- `- A% t; F: O模拟3次更新配置效果4 D# g: y/ g# _4 U4 L& `9 D0 I' i8 n
MailService{mailConfig=MailConfig{username='6bab8cea-9f4f-497d-a23a-92f15d0d6e34'}}; Z8 [" f5 M: x% M0 @2 T- i
MailService{mailConfig=MailConfig{username='581bf395-f6b8-4b87-84e6-83d3c7342ca2'}}
+ D2 G+ F; Y. _# x% TMailService{mailConfig=MailConfig{username='db337f54-20b0-4726-9e55-328530af6999'}}
; \7 y5 K* n6 R) V上面MailService输出了6次,前3次username的值都是一样的,后面3次username的值不一样了,说明修改配置起效了。
6 c0 V3 b1 ^  X( Q3 j4 b/ j& x( y9 V) |% E1 {& i  V! I
小结
) Z. i. W0 J& E9 m6 H0 `4 U) e' e" Q: G1 e
动态@Value实现的关键是@Scope中proxyMode参数,值为ScopedProxyMode.DEFAULT,会生成一个代理,通过这个代理来实现@Value动态刷新的效果,这个地方是关键。* E& A+ [; G2 v6 D

, h- n6 H9 D' S; o1 W6 l9 _有兴趣的可以去看一下springboot中的@RefreshScope注解源码,和我们上面自定义的@RefreshScope类似,实现原理类似的。4 ]$ u: F* x" e: o

% c7 s9 P0 W( Z总结0 r/ |& B8 ~5 y9 a% j
& `8 t5 q, V4 J) E
本次面试过程中3个问题,我们都搞定了,希望你也已经掌握了,有问题的欢迎给我留言,交流!7 T! x5 v* T, C
/ `+ @) `. m/ h/ h: L. R3 J
案例源码
- y% Y, z* [* W' X6 x- p, W% g( Y5 n6 e" H
https://gitee.com/javacode2018/spring-series
, n  [! c2 S: [: D/ V路人甲java所有案例代码以后都会放到这个上面,大家watch一下,可以持续关注动态。& ^7 m0 v: t& ?$ Y5 C
4 h5 ?! W* X" z6 Q  N
Spring系列
' Q! ?7 t2 r. U8 N) ^' ~1 B6 s
- J: p( z/ C+ |. h2 h& v8 p  gSpring系列第1篇:为何要学spring?, D2 x- Y# d% L3 E/ b3 H
" d3 E5 N3 g. {( D$ U& _$ `  U
Spring系列第2篇:控制反转(IoC)与依赖注入(DI)
0 I& C" ]. b) p$ O5 g1 K
- g5 W: P5 z) j9 g* f# jSpring系列第3篇:Spring容器基本使用及原理
! J( \8 d; y/ `
- K5 G0 m8 H3 c0 V/ g+ jSpring系列第4篇:xml中bean定义详解(-)
$ H" h4 q4 G2 r  L& J$ ]( |
# H1 K% [" I8 D, {2 e2 |( O8 {5 oSpring系列第5篇:创建bean实例这些方式你们都知道?" f" z# x& [. h* v' U; d$ u
' E2 i( @# ?5 Q! o: U
Spring系列第6篇:玩转bean scope,避免跳坑里!
7 P" \! E) t/ o- {4 r6 v' u1 \! T) J, \5 O. `3 a% |
Spring系列第7篇:依赖注入之手动注入2 x) i# [9 B7 v7 s9 N5 x, [

* Y; Y9 [! h4 ^, YSpring系列第8篇:自动注入(autowire)详解,高手在于坚持1 W. Y2 @9 J- |1 W
2 s, X0 y% Y- G! l# w2 K
Spring系列第9篇:depend-on到底是干什么的?; f# f$ H. m9 m# D7 d1 U2 j
; d! C! F1 P& l% g) x! \: m0 F1 D
Spring系列第10篇:primary可以解决什么问题?% a4 j, B) C7 F! e- p8 h
2 ]* K6 M9 k& j% T! O4 t- e
Spring系列第11篇:bean中的autowire-candidate又是干什么的?
9 l% q" s* W; o& S' M; i$ [: O
+ i) J: V2 [, b# h, `: bSpring系列第12篇:lazy-init:bean延迟初始化
/ V( @2 L& f/ p3 L/ V( S5 U2 E& `' x. e2 R! l' L' y& e+ W9 C; F
Spring系列第13篇:使用继承简化bean配置(abstract & parent)6 [# T- ~5 Q1 V) C
. Z: D, q; F# A4 P: u
Spring系列第14篇:lookup-method和replaced-method比较陌生,怎么玩的?5 J+ S% E* a7 M% d/ \

2 [. _& X% U' }Spring系列第15篇:代理详解(Java动态代理&cglib代理)?
) n# l, Q! t% G$ b7 s. t9 ~/ M: w2 b6 o9 S* o
Spring系列第16篇:深入理解java注解及spring对注解的增强(预备知识)! P( c9 H) p0 y# H5 ]: B

% B5 j! j% A& b! H$ C/ y; b( X% \Spring系列第17篇:@Configration和@Bean注解详解(bean批量注册)' H. F# ]" e) ]6 P5 u- x2 f; y

/ ?% S' J* k& U* }9 tSpring系列第18篇:@ComponentScan、@ComponentScans详解(bean批量注册)
: k7 \. x3 b  V# {
5 b$ ?! `+ T& m* GSpring系列第18篇:@import详解(bean批量注册)3 q0 Q* s1 o- t0 z4 n7 e

. W( I% H/ l' q/ Q/ P: _9 oSpring系列第20篇:@Conditional通过条件来控制bean的注册
6 w( z1 V  M* c' {7 O9 n0 `5 Z# g! P2 ~# q* s
Spring系列第21篇:注解实现依赖注入(@Autowired、@Resource、@Primary、@Qulifier)  @) @" p$ x( J: q

6 v/ |7 Q0 z: k4 ZSpring系列第22篇:@Scope、@DependsOn、@ImportResource、@Lazy 详解
! _6 L4 \& K& ]8 P; g7 p+ }+ Q' ^" s- e) U* Z" C+ X
Spring系列第23篇:Bean生命周期详解$ k" F: p( D% w, z
. q! E7 n  c/ j! z: p% u  u
Spring系列第24篇:父子容器详解
. K7 O( s0 H/ T/ x# r* l' W3 y# @5 j$ A5 z! g3 j- i
更多好文章6 m  l8 E6 P) P+ g" V5 F

$ g& w6 O, m' ^* R2 J! fJava高并发系列(共34篇)
# v7 E- X" M; @' t# Z+ W! a
, \# f4 i8 V( G% {  D3 F3 ~+ O0 lMySql高手系列(共27篇)
; O; r; X$ o9 g6 P% P# u' |2 [' K' ]* P5 y1 w5 ]$ Z/ a  [
Maven高手系列(共10篇)
4 ~. u' J$ @% f7 a% @" k) e7 \; m0 _; V" F4 L7 d: x" H& Y
Mybatis系列(共12篇)
& ?- |0 ^2 z$ q
# @% q- A, S; V* @) E0 _聊聊db和缓存一致性常见的实现方式* ]2 C3 b: j2 d: W
% d2 z9 a4 T4 X2 v' @8 z" Z4 w( D% D
接口幂等性这么重要,它是什么?怎么实现?
+ G% |9 L  k. n9 S! M2 ~9 w7 U+ o: @5 ?3 o) H' E) Q0 C5 e7 D
泛型,有点难度,会让很多人懵逼,那是因为你没有看这篇文章!5 R/ n- e2 v0 v! h* S/ A9 }, \
————————————————& x$ R% o. V( p; f
版权声明:本文为CSDN博主「路人甲Java」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
* I9 H4 @+ u2 C: K6 ?原文链接:https://blog.csdn.net/likun557/article/details/105648757
" E3 q. W! S) P: V, a% C1 d" U
$ J5 h! z% E8 Y( U( w) |; `5 ^
% i- r) |4 V7 ~/ m




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