数学建模社区-数学中国

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

作者: 杨利霞    时间: 2020-5-23 11:01
标题: 太狠了,疫情期间面试,一个问题砍了我5000!
太狠了,疫情期间面试,一个问题砍了我5000!
/ D1 K; z3 H3 _( ]" }0 j/ |疫情期间找工作确实有点难度,想拿到满意的薪资,确实要点实力啊!
* S0 G* z0 }- J* n* ^4 z& s2 W; w/ O$ L( g0 [) X
面试官:Spring中的@Value用过么,介绍一下
2 P) D; L  u# u1 z5 @$ [7 P
& N/ [  ^+ ]  O我:@Value可以标注在字段上面,可以将外部配置文件中的数据,比如可以将数据库的一些配置信息放在配置文件中,然后通过@Value的方式将其注入到bean的一些字段中/ `2 q1 g  N3 o+ }6 f1 ^
/ c/ o* |9 B. H7 \$ {  T6 m# [
面试官:那就是说@Value的数据来源于配置文件了?% N9 H. V6 {+ x# D* s. U

: c2 C" }% G& t8 {5 x9 i我:嗯,我们项目最常用更多就是通过@Value来引用Properties文件中的配置
3 i, f! j. j# ]) i
& J, H% c  ]  Q4 }4 o面试官:@Value数据来源还有其他方式么?1 s: h; Q3 n" u& A* j/ y

3 f* n7 [9 B3 X我:此时我异常开心,刚好问的我都研究过,我说:当然有,可以将配置信息放在db或者其他存储介质中,容器启动的时候,可以将这些信息加载到Environment中,@Value中应用的值最终是通过Environment来解析的,所以只需要扩展一下Environment就可以实现了。( w; }' s0 |, L% N% N- m& I
0 o& D. A& d7 j/ i' G
面试官:不错嘛,看来你对spring研究的还是可以,是不是喜欢研究spring源码?
8 j2 I' E. g6 F' \- e+ u
5 E+ y% @% N$ k( W% n, b  O我:笑着说,嗯,平时有空的时候确实喜欢捣鼓捣鼓源码,感觉自己对spring了解的还可以,不能算精通,也算是半精通吧3 N! n% r$ r2 M* ]; O9 J7 ?

' J, ?/ T% l& y* f, B; T8 P$ }面试官:看着我笑了笑,那@Value的注入的值可以动态刷新么?
$ o0 d( P- e6 ?" t6 W& r/ S
$ v, ~- I1 x7 p7 b1 y我:应该可以吧,我记得springboot中有个@RefreshScope注解就可以实现你说的这个功能- ^! j4 z$ m9 |
  I5 u- ]% T4 L# u$ |
面试官:那你可以说一下@RefreshScope是如何实现的么,可以大概介绍一下?3 ]+ a! N/ \' [% @" Y
) M( T8 C+ R4 b; u9 \% O) O
我:嗯。。。这个之前看过一点,不过没有看懂* U* ]" j2 D! P
8 u" u( k( a! l% S7 C
面试官:没关系,你可以回去了再研究一下;你期望工资多少?* K2 n; M# b* y/ Z$ B, M- l

7 O# A- s; P5 ^我:3万吧
7 b  I# z* v# R9 d
; M& |* p, C! M8 e面试官:今天的面试还算是可以的,不过如果@RefreshScope能回答上来就更好了,这块是个加分项,不过也确实有点难度,2.5万如何?
& o9 b9 b$ g; j7 c) G; @  ]/ H5 T5 y4 Y
我:(心中默默想了想:2.5万,就是一个问题没有回答好,砍了5000,有点狠啊,我要回去再研究研究,3万肯定是没问题的),我说:最低2.9万+ Z8 o$ @! E/ c
5 D. K+ E$ R- F6 @7 o; D; W5 M4 x
面试官:那谢谢你,今天面试就到这里,出门右拐,不送!
; O/ y+ f4 d. k# [1 I: T  p2 j
6 m. I: q# x. S5 K我有个好习惯,每次面试回去之后,都会进行复盘,把没有搞定的问题一定要想办法搞定,这样才不虚。
4 d: l8 l" e5 o( [. W& X+ `
( s# v# o6 Y, L这次面试问题如下
6 _7 ~5 f* a" y: b5 `5 W& [0 j6 e, J9 X: K
@Value的用法
" x6 Y# i9 j8 v' H. r, L6 ]: z5 C+ F4 L6 C
@Value数据来源
/ U* A0 I, j! Z* B  c! q
- g+ ^! Z. J/ |3 ?% J! h@Value动态刷新的问题
: B+ w# q5 ]! A; g) ^, D; b1 n. f6 }3 ~' T" Y: C( b
下面我们一个个来整理一下,将这几个问题搞定,助大家在疫情期间面试能够过关斩将,拿高薪。  W% N5 D$ q" w: Y
! Z6 a! a: S9 {. g1 T
@Value的用法
" u* l# o. @& i" e* g' Q, _# p/ R* u$ E+ {* ?% u: z/ D3 P
系统中需要连接db,连接db有很多配置信息。: E2 I0 t& P: C" _3 v! b% `

9 C  L! z7 C1 o5 g. V; U' l系统中需要发送邮件,发送邮件需要配置邮件服务器的信息。: U( R( ?: {! H1 Q( e# `! i
2 O' e( U8 P( m7 V3 J
还有其他的一些配置信息。0 |& A6 L- T8 p

- m3 i5 N0 B* r我们可以将这些配置信息统一放在一个配置文件中,上线的时候由运维统一修改。2 l( e' m& R/ C1 @

1 R  R8 K/ p  c* |/ [% v; B那么系统中如何使用这些配置信息呢,spring中提供了@Value注解来解决这个问题。1 j, `0 Z4 n% M' ^

0 s! R. h8 ]+ {通常我们会将配置信息以key=value的形式存储在properties配置文件中。
! s+ M6 ]# p7 \$ {1 v
) O& t1 e" r; ]8 G通过@Value("${配置文件中的key}")来引用指定的key对应的value。
! {; P4 r! P7 |3 }; r/ U/ N4 b1 s; |' U8 X( i+ ~# ^' A  T
@Value使用步骤
! L$ a, s# x- [. Y
: }4 |- z% L' o2 |4 G# g, {$ z0 r步骤一:使用@PropertySource注解引入配置文件; `$ u' ^4 F3 L9 m: k" \/ `$ y

: Y$ I% Q- }+ x! H将@PropertySource放在类上面,如下
) h! i( e# p% h# M, T6 n3 N$ R, j( V  X7 _
@PropertySource({"配置文件路径1","配置文件路径2"...})) H& A8 `4 T. A2 O' r  r
@PropertySource注解有个value属性,字符串数组类型,可以用来指定多个配置文件的路径。
' d* f: d4 V4 L5 X! i
8 L7 f1 o& p1 O如:
1 Q; h' F& h3 ]  X( F  L: x: [$ }
6 R1 L5 O6 [: z" q+ a; E! }@Component
; O' W% V8 {$ q1 M0 K@PropertySource({"classpath:com/javacode2018/lesson002/demo18/db.properties"})
2 v9 @4 D% D3 K1 `9 ]public class DbConfig {/ z2 T1 Q9 f) F3 W0 c% B9 ^& q- J
}
3 e( ^7 x0 J& n& u/ c& x' w步骤二:使用@Value注解引用配置文件的值
4 o9 M& v7 V3 p1 D0 d2 m8 s5 l& I/ w: e9 Y* F2 u! _) O1 o
通过@Value引用上面配置文件中的值:! B' \; u2 {- ^2 F+ x9 r

( A0 M6 e; r7 ^  c语法/ N$ R4 \9 ~. V5 d, X& @

, M( L* v/ I* J@Value("${配置文件中的key:默认值}")# d0 j& l- t. o1 h
@Value("${配置文件中的key}")0 \* Q7 u7 e) a; f+ a9 S
如:
$ U/ B7 M, C( q4 v
! ?& ?8 C" x- [. ]$ q4 d& Y9 C1 a@Value("${password:123}")
1 J9 M6 m# ]! I' |6 ?. H上面如果password不存在,将123作为值
7 n$ F0 {; y: ?3 w5 u) _0 T
# B- j& _9 f! u5 ^1 P- m$ X( s@Value("${password}")# d2 ^: ]+ Y( G& w( O
上面如果password不存在,值为${password}6 K( O8 v# i( ~% W

% Y8 o/ D* t# Z8 K假如配置文件如下6 n, i4 D/ `3 q6 m7 u4 p

, c# O  B  Q% f7 ~8 h" J( ]9 yjdbc.url=jdbc:mysql://localhost:3306/javacode2018?characterEncoding=UTF-8
  {* a' q( e! S  p6 g/ ^jdbc.username=javacode
/ k5 u# p- @7 T4 qjdbc.password=javacode
3 R* L4 j# o2 ^, M使用方式如下:
0 y) U* J  C1 d9 k# I
# o7 Y7 G/ s( m9 ?@Value("${jdbc.url}")
; |) G2 u4 k. L) W3 W5 ^. [private String url;
! K# J! j) m/ r. x  Y- h# |9 U( F: W# L3 n7 W) [1 w. {8 Q% r
@Value("${jdbc.username}")
2 v8 s- P8 p. L# C# }0 Pprivate String username;
+ x; b- O7 t. b! I6 Z) |2 |" V! H: p/ |: z9 @1 |9 k% M% i& q% t3 {) Y( j' n
@Value("${jdbc.password}")
0 y: r! H/ S. Q% w% R1 E4 gprivate String password;  u) G. }% ^# E2 K' n0 ]
下面来看案例: B2 j$ V/ W; T6 C. R$ \' @3 D
: Z5 c8 P5 E4 k; h! P: t
案例  H7 H' H% p: a+ |# D# o/ H

, S- L3 g( u& L* \! W% ?+ u来个配置文件db.properties& ]% s: z( l- F+ v; |% {
2 K- L* D& ~7 T
jdbc.url=jdbc:mysql://localhost:3306/javacode2018?characterEncoding=UTF-8" K1 z0 I5 u0 s+ a8 K8 y. x
jdbc.username=javacode
$ f+ ~+ P5 |& c1 P5 V& t1 Mjdbc.password=javacode( I4 [& }8 u; Z" l( c/ M
来个配置类,使用@PropertySource引入上面的配置文件/ c3 X1 [! m* Y% d( }. W+ e2 F

, i! v9 \1 d+ t" |7 c  Jpackage com.javacode2018.lesson002.demo18.test1;
* s! l/ d& N% J0 L) m: e0 [  s0 Y0 C% q  j/ d6 _+ }
import org.springframework.beans.factory.annotation.Configurable;) C+ A* H$ X& _4 Y! Q+ h) Q
import org.springframework.context.annotation.ComponentScan;
: |7 [$ x6 j/ dimport org.springframework.context.annotation.PropertySource;
% T6 Y7 ~4 N' i/ l' a$ p* D  z+ v0 T* {' t4 ~& p
@Configurable8 P( k7 U8 B( i
@ComponentScan! z1 a! ?# P8 U5 c. S
@PropertySource({"classpath:com/javacode2018/lesson002/demo18/db.properties"})
& G, {1 }0 B8 ?& M3 G9 m' }public class MainConfig1 {9 @. L1 K- Y8 ?
}- m: J  b% R; ]
来个类,使用@Value来使用配置文件中的信息( D0 B( m. n$ m+ A# v
+ E, f3 b9 P9 u% f1 H0 y
package com.javacode2018.lesson002.demo18.test1;; L" p  n2 K+ |* }
7 A) H0 ?1 P. I9 I6 N( q
import org.springframework.beans.factory.annotation.Value;4 I6 x1 E% m& x4 D' N
import org.springframework.stereotype.Component;
  @$ r0 k9 q/ N  t0 P: `( i0 ]3 B( y/ G
@Component  f, W; L. P! D$ C
public class DbConfig {
7 ^7 O8 D/ ^1 {# X  g7 G' k+ F# Z+ U0 n: u% Q! Z
    @Value("${jdbc.url}")4 g) ~6 G5 O5 V' c3 j: O* P5 ^
    private String url;
) t; O- C, T1 g! ]1 C
6 c* r  Q. Z9 J2 U* H8 L* @" }    @Value("${jdbc.username}")5 p, q5 j( @7 d0 F' Y; {" }  x! I
    private String username;
/ ]$ k9 U' {& a3 o) n2 @% X- u+ k& j* s5 F
    @Value("${jdbc.password}")
+ w3 q" S- q. b1 X5 j6 a4 s% W    private String password;
6 o* n9 h: j% Q3 Q" U# Z$ V2 B7 C
+ N3 N( @* Q! z9 y4 |- ?5 F. L    public String getUrl() {. c: q- u( J5 M( f8 c$ }, T
        return url;9 k2 W4 P/ F4 w6 b) g
    }
7 ]1 x6 o* W- ~" H+ g2 \% I1 P/ D' u0 T: h0 J/ N$ F
    public void setUrl(String url) {% ^  C5 u) o- X
        this.url = url;8 P* m# H* |7 K+ c. W
    }: D' h4 S: ^; H) v) c: p- P, |

% N; B  |( m" c* [4 D    public String getUsername() {# C9 L8 q% E$ D
        return username;* C; m% N" C: Z0 h  z
    }$ P7 P4 ?" [, ~, T

0 f* f; Y& J6 u2 @% H6 b( m    public void setUsername(String username) {* ?" q/ F" ?$ C* a$ B
        this.username = username;
" A% P! w  z" u0 p9 t9 u! T    }
1 y$ _5 ~; ]+ u4 h5 _  G- c  l( h& G% A  f% r% c1 Y  R
    public String getPassword() {( r) A4 m5 q  j: h/ s  D
        return password;  n- d0 N0 `3 k
    }
' Y* E, x% t* R  W
' w! [. S5 g- B+ L/ t9 k: I    public void setPassword(String password) {
  Y0 p' L. C6 V' u        this.password = password;
: d: ~0 `0 g# g1 j9 k) k  \8 I    }8 h2 n" A* S/ j

( o+ L6 k) n  \7 a2 g6 o. O    @Override
' ^) O7 D6 d3 t! Q% u    public String toString() {0 g& l9 X; X  r# w
        return "DbConfig{" +
1 R0 {' `* V0 i8 W' o                "url='" + url + '\'' +
  K6 x* X) ~# O/ }                ", username='" + username + '\'' +6 d# K  |  s' g* t* a  Q2 L
                ", password='" + password + '\'' +! Y  q% j8 P, n- W; [
                '}';
) l& c& G' `( O5 z, E5 K    }
' L$ N: z& i4 F# o7 P}
9 P. N4 s3 q7 Q, R上面重点在于注解@Value注解,注意@Value注解中的, m+ U: Z0 u7 l3 T' }( P* j4 ~$ B* w& k; B
. e/ _/ v! R/ A
来个测试用例2 E1 A3 z- U4 {" f/ S1 K

5 ^' a1 C0 J$ d4 \+ Ppackage com.javacode2018.lesson002.demo18;: ]7 y& l$ ?- K6 @. X
8 w+ m8 }1 z' @9 O# R- z  C
import com.javacode2018.lesson002.demo18.test1.DbConfig;
% [/ b8 y) g" y. \import com.javacode2018.lesson002.demo18.test1.MainConfig1;
" F  H* A; }1 Q( M3 [) r; \/ ~! x: ximport org.junit.Test;, X: [* ?# B2 n6 M7 L
import org.springframework.context.annotation.AnnotationConfigApplicationContext;" L: x& m6 ?/ ~  p" }- }
( |' V  _0 I! g& W& p
public class ValueTest {
2 @9 u! T- H0 P4 B. Y
. u+ g: W+ W: |0 T, \! a    @Test( K! Q6 Q4 q4 c2 F/ M
    public void test1() {$ J1 }; [: k5 d" L$ J
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();/ z2 ]3 L9 m4 u5 t
        context.register(MainConfig1.class);
; k& w5 Z( h) {8 s+ I" r9 E        context.refresh();
* v, G$ {, g1 C/ z+ \; X) d8 V
        DbConfig dbConfig = context.getBean(DbConfig.class);
9 }4 S) A9 J& k& J* d6 N        System.out.println(dbConfig);
: Z. B2 g  o- f) o0 P    }
7 g: v/ ]6 l  t" @- h" l}
! v* }% s+ \9 \- k' Y5 [3 K运行输出. y( o/ M- Y1 C! W1 k3 G: L
% a2 }6 O- q$ g
DbConfig{url='jdbc:mysql://localhost:3306/javacode2018?characterEncoding=UTF-8', username='javacode', password='javacode'}
" [, b3 J" F- v9 u. I8 N0 M! W上面用起来比较简单,很多用过的人看一眼就懂了,这也是第一个问题,多数人都是ok的,下面来看@Value中数据来源除了配置文件的方式,是否还有其他方式。% j$ c6 a& a7 S- {6 H4 `% M

5 ~: W  z4 B2 U4 ?/ o" ^; w) m@Value数据来源, w$ t$ t8 g7 ~
' ~7 a& E  b; [) b4 F
通常情况下我们@Value的数据来源于配置文件,不过,还可以用其他方式,比如我们可以将配置文件的内容放在数据库,这样修改起来更容易一些。: G5 N4 O. [- G) @+ Q

, r* V0 N: m+ l9 |# t. F我们需要先了解一下@Value中数据来源于spring的什么地方。! `7 g9 N  j8 r" r: J

" D0 d+ y2 G2 f& z2 d; t  J: Nspring中有个类, P* e( j/ w7 p1 r- X: I' {

6 A  h& O6 E; C9 `- N: corg.springframework.core.env.PropertySource8 A# Y! r1 R2 F" W( X; E
可以将其理解为一个配置源,里面包含了key->value的配置信息,可以通过这个类中提供的方法获取key对应的value信息7 R9 I1 h+ K  y
, I7 q( p: E4 B! x- b( e" V
内部有个方法:5 S0 B9 w$ Y9 b

6 I* g: x& P0 O( hpublic abstract Object getProperty(String name);# s! f% y  \( P$ x" ~+ H+ m
通过name获取对应的配置信息。
: L4 d! F! W6 ], l. h+ ~7 ~) Z& M! t5 A7 b
系统有个比较重要的接口7 E/ f7 W8 L) `1 r3 D/ ]7 U$ h

' H: Y( m- M7 b: z- u( Norg.springframework.core.env.Environment
# {! I5 \( e" s* }1 }$ M% u. ~用来表示环境配置信息,这个接口有几个方法比较重要* z5 X8 Q$ Q8 p, a
5 f# `, ]* b5 |, U
String resolvePlaceholders(String text);6 N! C1 {* d( ?8 K$ V  D; C
MutablePropertySources getPropertySources();3 v7 D. l; W$ r; g) J
resolvePlaceholders用来解析${text}的,@Value注解最后就是调用这个方法来解析的。2 s! [6 ~1 P1 f

) ?7 T' u% C! U. J! ggetPropertySources返回MutablePropertySources对象,来看一下这个类
+ ~: j4 }6 S: I0 j
* H) z; r8 b1 J. h( C3 Opublic class MutablePropertySources implements PropertySources {
5 J# f6 p% ~; }* k& ?
& e* j- ]: ]7 J- u4 N4 A    private final List<PropertySource<?>> propertySourceList = new CopyOnWriteArrayList<>();0 w* x, \4 U, I" F7 v; }

6 R, m9 [; S  x. B' x" o}. Q. ]5 R9 r- V4 \' k
内部包含一个propertySourceList列表。& D" l2 R5 g7 c9 z: p: o
4 W9 g- p: q1 U5 j% b) ]
spring容器中会有一个Environment对象,最后会调用这个对象的resolvePlaceholders方法解析@Value。
7 u2 R. }, \, m+ h! S4 \, t& u. l- `$ {
大家可以捋一下,最终解析@Value的过程:
' P3 U9 i5 j1 V
, M/ U* o& I" p/ C$ ]; r; k+ y1. 将@Value注解的value参数值作为Environment.resolvePlaceholders方法参数进行解析
1 \' m' c! f' h" n( z( C2. Environment内部会访问MutablePropertySources来解析. h2 B# ~3 H  k
3. MutablePropertySources内部有多个PropertySource,此时会遍历PropertySource列表,调用PropertySource.getProperty方法来解析key对应的值
5 d" t- J# G: R  j; \  C3 D6 ^2 Y通过上面过程,如果我们想改变@Value数据的来源,只需要将配置信息包装为PropertySource对象,丢到Environment中的MutablePropertySources内部就可以了。9 H% T( D( ]  x8 x  [) _! C

% ?9 i; }3 b( d  C5 z下面我们就按照这个思路来一个。+ ^7 {! ^1 k) N6 }$ D+ c  m

) c, A7 k) F9 L) k0 h/ m8 m来个邮件配置信息类,内部使用@Value注入邮件配置信息
6 |% f) ~) ^# @) J6 O  M' f5 A! Y' U# V0 |( w3 V
package com.javacode2018.lesson002.demo18.test2;! \# f6 u" V2 `3 a  e. h. H
, U; x) b1 U" B5 g; f! T, F
import org.springframework.beans.factory.annotation.Value;4 S7 ?' E$ G; D; q) g' L
import org.springframework.stereotype.Component;% L  h. _. [9 {, |0 x4 q

8 K0 V7 [3 U; C/**
, D" ?& \+ m2 a) p- n7 @; x2 B * 邮件配置信息0 \1 @4 Y1 s2 R& V( R" y/ K3 v
*/
3 q% A& G' w: X7 \( Y@Component  x$ w% Z1 T2 h' f2 l
public class MailConfig {
  L$ ?, [; O, m5 f; s( T6 R6 B  J& O/ A3 c
    @Value("${mail.host}")
' ?2 U( _" P( ^: [, ^7 T; f9 l- O+ J    private String host;' I4 b5 X- C; a% _9 E
. t) p7 P. u9 T
    @Value("${mail.username}")5 d) h$ E9 H  l" B. ]# X
    private String username;, R) l# j, F2 |, J& n9 I8 m$ E, k# x

8 R( n1 S, C! p8 Z, p    @Value("${mail.password}")3 m! R% N; S; g) q+ d3 G" b
    private String password;
  c7 k5 C' |0 }% [6 `+ n5 D+ z
! \$ [) L: _" W# e    public String getHost() {
  H# p! b6 O5 _% S7 J  B+ P- {$ ^        return host;
  Y6 R0 Q- \- L0 |& p! [    }# f' H& U6 ~2 ^/ g9 K, r6 {) ^  p
$ M4 y  x- C1 y9 c1 Q
    public void setHost(String host) {
" K7 X" _- ~  C6 L7 A- h        this.host = host;+ }: \: n0 a% f8 ?6 P$ s! Z: Z" v
    }
) m, P0 M! H; j) [
" M) y  j+ A& R; b, g/ v    public String getUsername() {
) Y* b/ [/ ]' d        return username;
2 M. F' x# D1 w0 s- F    }
! G& j' @, Z/ k- D2 ?  y. I- Q) j
    public void setUsername(String username) {3 Z7 h1 z, T% t" @
        this.username = username;' i3 S+ I3 b5 s
    }! T  T1 w- y; U3 h; \- Q
) x  n4 F0 K4 h' w/ c
    public String getPassword() {4 L3 A, N( C% `' d
        return password;+ g% H0 j. q$ T8 R" n* M3 R
    }
7 J9 _% B* S' S5 }% d( b' ^/ L: a; k. s' I$ m0 e
    public void setPassword(String password) {
8 g+ ^: J# s9 N  _' m& j8 I, Z5 j        this.password = password;% W/ t/ I0 ?( l' z( a, S
    }: i5 s2 D& b9 T# U; G: a

# L  v" ?( a/ C6 {3 {' m( |    @Override
0 g( B5 y3 ?) I& }- l    public String toString() {% J9 n2 U9 k+ x/ l. Q  k
        return "MailConfig{" +: G" Y- L; \9 Q: ]
                "host='" + host + '\'' +" M; l1 v4 |- i+ F
                ", username='" + username + '\'' +) l* i( q) u9 s/ t6 y) D) w
                ", password='" + password + '\'' +, k% n. ~; v4 o. _, ]
                '}';
8 [! `! R: O8 D6 Q1 W& A    }
1 r7 g+ j# }2 K% N/ v}
6 h& E$ L: d: ^& V再来个类DbUtil,getMailInfoFromDb方法模拟从db中获取邮件配置信息,存放在map中
- S$ I$ r& k: o" h% `! n( R5 f
0 }2 e- ^3 E( E4 w, S& k1 mpackage com.javacode2018.lesson002.demo18.test2;
) P6 C7 V* L8 Y; t/ H, b& H5 o6 m
: v. `, c1 v. B& A+ l( v! gimport java.util.HashMap;  i2 p. n" K1 H4 y
import java.util.Map;) n# B( t6 E+ Q( @( t

2 V. W1 |) O, u9 ppublic class DbUtil {
! {# C% i7 z3 D6 Y    /**
5 k* u& E1 _8 a* f4 ^# J     * 模拟从db中获取邮件配置信息2 T0 v" e; \6 _/ t2 e! s- }" C
     *7 k8 N( v) m& K9 f, U: L! m( B
     * @return
3 Q2 ~& M0 ?  L! x; @6 ~! }& X     */. v1 }: N1 r- v4 j5 \8 z5 |
    public static Map<String, Object> getMailInfoFromDb() {
2 O. [3 b( b( T/ W/ o        Map<String, Object> result = new HashMap<>();
/ f/ T' J+ X1 \+ a5 p6 S, b' V        result.put("mail.host", "smtp.qq.com");0 q4 {% t6 U5 f# F! b* J
        result.put("mail.username", "路人");+ z! X0 v9 k7 Q! {# H4 s
        result.put("mail.password", "123");5 w, n* p6 _( E! }+ j6 A
        return result;' l$ ]1 O) w. `( T$ b" \% m
    }
! ~+ O- v2 |5 d1 _0 n% D}& C' q& c/ c& ]; R1 J2 z. |
来个spring配置类2 k: p0 ~; v$ V( x9 A' K1 J% k- D& I

: A+ n1 k! h+ ]4 @; `4 T2 Jpackage com.javacode2018.lesson002.demo18.test2;
) w. c  [' |" M1 s0 k' B& f
4 d2 f* G% s5 {) {# I* o: ?7 Nimport org.springframework.context.annotation.ComponentScan;
$ n2 M- A' l" \# i  mimport org.springframework.context.annotation.Configuration;
( M. `% S% p3 @# \* S- ]/ m7 j6 Z) n  l# Z
@Configuration2 f/ D- L, [) x% I
@ComponentScan% W0 U8 g9 \% t3 ~) r, @
public class MainConfig2 {
& d, f; z6 l) ?* }! g$ w9 ]}$ C! @8 v+ W+ M/ m9 S/ o
下面是重点代码! ]1 ^( i  W6 T$ n' i* d
' H! o% B5 e7 y2 k- P
@Test) a/ P6 g* r4 D% z) ?) b# l
public void test2() {$ L' N2 _- Q+ q* I) Y
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
2 l0 T* `( G$ R8 k
: Q$ C& R- e8 ~    /*下面这段是关键 start*/
! O' R& S0 l2 r( e" T) m    //模拟从db中获取配置信息
2 e" Q3 J% Q( y' |: k: T+ M    Map<String, Object> mailInfoFromDb = DbUtil.getMailInfoFromDb();( B- N& F( C' s4 @6 }6 o5 k
    //将其丢在MapPropertySource中(MapPropertySource类是spring提供的一个类,是PropertySource的子类)
: c6 P& _' ~) s. V    MapPropertySource mailPropertySource = new MapPropertySource("mail", mailInfoFromDb);  l/ D) x- X' t% x5 }" v
    //将mailPropertySource丢在Environment中的PropertySource列表的第一个中,让优先级最高! w; {% X% n6 ?! Q, D
    context.getEnvironment().getPropertySources().addFirst(mailPropertySource);
, n/ K4 Z' @# f3 R$ ^6 P    /*上面这段是关键 end*/
: `% B, @9 m. \) |( P
& d: d$ I6 K9 ^& D    context.register(MainConfig2.class);
- V% x- N0 _( H9 X    context.refresh();
/ g( e3 S1 J6 ~; e  `" `* B& O    MailConfig mailConfig = context.getBean(MailConfig.class);
; ^9 l2 E4 k7 c1 p) o/ c$ h    System.out.println(mailConfig);
, J- m. \9 m: `4 h8 E$ A6 s}- Y1 y- @" [$ R. y  F9 d: o' H3 F8 g
注释比较详细,就不详细解释了。; b7 |% F- |' V/ p# Q- N( Y% }0 T
& E  \# m: q8 ?+ C: M
直接运行,看效果
. t& m3 X* b+ t* m+ A) \  V2 `% l. A
MailConfig{host='smtp.qq.com', username='路人', password='123'}2 ]# w7 t/ _+ D) T- V
有没有感觉很爽,此时你们可以随意修改DbUtil.getMailInfoFromDb,具体数据是从db中来,来时从redis或者其他介质中来,任由大家发挥。$ n3 H# C' @, S
9 g$ y+ U. E* ^6 o, M
上面重点是下面这段代码,大家需要理解, q7 W: @/ G, K# t0 N

1 o  N$ {& k5 O% \2 b/*下面这段是关键 start*/- S6 S  Z8 Q' e5 @7 W* s
//模拟从db中获取配置信息
0 \/ D. h3 ^0 O' C5 N+ ~: EMap<String, Object> mailInfoFromDb = DbUtil.getMailInfoFromDb();1 C  `4 \0 Y7 q/ {& d  P- o0 t( H
//将其丢在MapPropertySource中(MapPropertySource类是spring提供的一个类,是PropertySource的子类)
0 K1 @7 r6 h; {+ S. J7 r3 [MapPropertySource mailPropertySource = new MapPropertySource("mail", mailInfoFromDb);- O- E' N6 q4 F, m2 v8 C. j- ?
//将mailPropertySource丢在Environment中的PropertySource列表的第一个中,让优先级最高
, D6 P8 l& Z, m; \7 {context.getEnvironment().getPropertySources().addFirst(mailPropertySource);
4 M; U$ R( f1 @* J6 t6 ]/*上面这段是关键 end*/* V4 r/ ^+ f. h* b+ q9 B
咱们继续看下一个问题# [9 |# ]$ H, _+ q1 ~
, _4 V0 C+ {% R
如果我们将配置信息放在db中,可能我们会通过一个界面来修改这些配置信息,然后保存之后,希望系统在不重启的情况下,让这些值在spring容器中立即生效。3 u1 ?8 x6 `1 ]. V/ q6 n. S
/ }8 ?+ k# G, v" I4 T  a3 I
@Value动态刷新的问题的问题,springboot中使用@RefreshScope实现了。3 Q8 \2 Y* F5 [, B
. Z% L  e$ Y# N- }5 j1 k; z
实现@Value动态刷新
' [7 v* g3 c2 o1 l
6 s4 e2 b2 S/ t2 g; Y7 W先了解一个知识点- m, N3 i4 y3 N0 g3 `! }1 C* G

  d+ F3 F# z* B) c- H这块需要先讲一个知识点,用到的不是太多,所以很多人估计不太了解,但是非常重要的一个点,我们来看一下。6 L! T5 x8 r: e7 O, x4 o+ o

- W- \( V  l% K9 c+ w0 e4 A8 y7 h这个知识点是自定义bean作用域,对这块不了解的先看一下这篇文章:bean作用域详解6 C8 \+ V% W% B4 R* E, l' N: l
! [0 W2 ~8 O# _2 o
bean作用域中有个地方没有讲,来看一下@Scope这个注解的源码,有个参数是:. r5 e) J1 \$ }1 m0 ]

) }/ O6 A: o: N: X7 hScopedProxyMode proxyMode() default ScopedProxyMode.DEFAULT;
. n5 {& ]# \, _! f) ^这个参数的值是个ScopedProxyMode类型的枚举,值有下面4中5 k% [' g  e6 q: e3 @4 Z
' ?: D9 e3 @9 _! P' o- J0 g
public enum ScopedProxyMode {
: m& @. M! d- L2 ]    DEFAULT,
6 g# B$ E6 V0 H; E    NO,6 Y+ S) S* o5 z& J
    INTERFACES,
3 {2 e: s7 M# c; ?$ W    TARGET_CLASS;
8 p6 j% p3 y# H# Z}
1 a3 ~' b! w( N前面3个,不讲了,直接讲最后一个值是干什么的。
# h9 {, Q9 \5 P/ @7 Q- M, h' A: b
当@Scope中proxyMode为TARGET_CLASS的时候,会给当前创建的bean通过cglib生成一个代理对象,通过这个代理对象来访问目标bean对象。
* _0 W2 h9 k$ d* n5 o" F3 N7 E" u* q3 e6 P
理解起来比较晦涩,还是来看代码吧,容易理解一些,来个自定义的Scope案例。) u. \3 P8 e9 `# B

! F: o' d( S! t8 l5 d2 v; |自定义一个bean作用域的注解7 ]- d% h# W7 s* H$ N/ U
9 @+ O# P+ J, I' i6 C4 x% h
package com.javacode2018.lesson002.demo18.test3;3 [* S- N" Y3 X+ k0 V

/ V: s- S. h( O; `2 U+ J% t5 o5 U6 ]import org.springframework.context.annotation.Scope;
! }( f, G8 c$ f( ^0 B0 Yimport org.springframework.context.annotation.ScopedProxyMode;
; Z7 I4 c, u8 u9 I0 N. n5 ~/ L
# m7 l* j2 c7 e$ [) X1 Timport java.lang.annotation.*;
/ c8 r' ~6 a4 w2 y( L$ y. m' d( H2 R
@Target({ElementType.TYPE, ElementType.METHOD})
2 n! e) p8 ?) ~  Y/ ~) i@Retention(RetentionPolicy.RUNTIME)
& _. N4 a! i% W@Documented8 n4 I4 X/ n- o, z+ ]
@Scope(BeanMyScope.SCOPE_MY) //@1' w7 s8 {, z' J5 P
public @interface MyScope {, {* B. q- b- H# @* V$ j
    /**! w! L  M5 A3 G; B$ m* B2 B; x  c
     * @see Scope#proxyMode(): D8 T0 _$ `' E
     */
8 \; O, r; R7 K9 ^' {    ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;//@2% L5 t3 k* n" V& ?3 k" l
}' `, e2 N& r5 _% ]; Z: X
@1:使用了@Scope注解,value为引用了一个常量,值为my,一会下面可以看到。8 Z7 C# N' {/ z2 `. f( H5 S/ ^

. D- d9 Y9 {/ h4 h( F$ X3 L5 I@2:注意这个地方,参数名称也是proxyMode,类型也是ScopedProxyMode,而@Scope注解中有个和这个同样类型的参数,spring容器解析的时候,会将这个参数的值赋给@MyScope注解上面的@Scope注解的proxyMode参数,所以此处我们设置proxyMode值,最后的效果就是直接改变了@Scope中proxyMode参数的值。此处默认值取的是ScopedProxyMode.TARGET_CLASS, n1 A6 D; g) B( t) D1 n
4 w* V: [! h+ t- D
@MyScope注解对应的Scope实现如下
7 V7 s) _! [) P3 ?( E
; M% P! Y( ~% apackage com.javacode2018.lesson002.demo18.test3;
2 U' B) ]8 n0 t: \& [
4 n  ]" H  ^) i% Q4 fimport org.springframework.beans.factory.ObjectFactory;" }3 ~( L. b1 [* q/ z2 B
import org.springframework.beans.factory.config.Scope;
0 `8 o2 Y* v: w* X# x; J' pimport org.springframework.lang.Nullable;
. z& {, s" q- V1 m+ j( l) c/ a' g$ C8 Q0 j5 k% U
/**
" O9 H2 k# f# I9 f8 [! S * @see MyScope 作用域的实现
2 N% F4 T2 a4 E4 Y5 c$ { */
0 v1 D9 z% m$ ]* {( Epublic class BeanMyScope implements Scope {
1 g. D9 m  q0 U% l- r  r; G. }
! x! e/ V) E. n    public static final String SCOPE_MY = "my"; //@1* }1 w. q+ N& |: H$ i" T) p
6 i: M, c6 ^; o* N) p, @
    @Override
2 z% M- o6 _( d, L" N' V  v    public Object get(String name, ObjectFactory<?> objectFactory) { " G/ ^+ i; h1 \. S5 O- g
        System.out.println("BeanMyScope >>>>>>>>> get:" + name); //@21 h  m% W, V3 Z) m, i4 M  C/ U
        return objectFactory.getObject(); //@3
( M4 j0 E5 U# b: M/ O/ |    }
) o* g9 v  \& v7 W/ K
1 D7 I' E; G4 v& K    @Nullable0 u4 H4 r: k4 m0 h/ v4 q
    @Override7 k" w9 D5 T4 Q
    public Object remove(String name) {
& s" I& F0 A( O9 [  G1 c' }        return null;
1 [0 Q2 O0 N5 q0 P& k, Z. `) d    }" w. ?" R' h. _0 Z/ q

4 t8 `/ n1 \; g( ]/ {3 m6 c5 \    @Override8 R% ?1 I3 b$ t( J& h: C
    public void registerDestructionCallback(String name, Runnable callback) {" z! n: c+ v6 c+ V2 f5 q/ S' ^, S
5 R$ o9 F0 v, _# B" }% i; G# A8 s
    }
) D9 Z! P9 l- l! U$ A( _, j: M, k: a
    @Nullable
) k3 X! \" U5 H  o( P3 P    @Override* A; p- @' Y' n7 V8 C* e% h
    public Object resolveContextualObject(String key) {- l' c" L" b  Z' ^; Q5 g$ Z0 L
        return null;7 m& r* ~. B' M! B
    }- i) N8 f" B4 @# c/ P+ z5 ^, [2 {6 F
5 v% X4 K8 {: z/ C
    @Nullable
8 i. ]+ V2 K' S3 h, t% z: b/ z    @Override  x7 b* f; N0 T# ^. K3 u
    public String getConversationId() {
% z  O5 J' p% m5 \! D( e+ ~        return null;
* s. {+ g% q8 D) x1 b1 `    }
; ]4 e0 Y9 U6 H, N& E}% S8 E! f  `; F7 {" d3 ^6 E
@1:定义了一个常量,作为作用域的值
% S/ a( y1 f1 H7 y/ n9 Y( }2 I+ z0 ~& ~- y$ n
@2:这个get方法是关键,自定义作用域会自动调用这个get方法来创建bean对象,这个地方输出了一行日志,为了一会方便看效果
8 b9 r8 |  d  B3 G; N( p& n0 q4 N+ c, |$ `
@3:通过objectFactory.getObject()获取bean实例返回。! [8 C- r0 L3 q1 G3 L, ]

3 d. Q. g( K; a3 b下面来创建个类,作用域为上面自定义的作用域+ m$ A& s+ H8 R. B4 M+ R8 J" ?* k8 U

9 d3 o; N7 X  a/ S, Bpackage com.javacode2018.lesson002.demo18.test3;+ A$ G! J6 X4 e5 v/ ~; {8 V

) M+ Y6 v4 M. x; R6 `import org.springframework.stereotype.Component;/ S( p8 p+ @* r" L2 U9 C: ~6 m
3 M2 |; H% o0 J. ^& A3 L& h" L
import java.util.UUID;
& X0 A9 Z6 z2 D! v: C5 `$ F+ C& R5 J# z8 f1 z8 I4 k5 c% e
@Component0 X( z& F( A/ y0 E' x5 d) \' b; n4 O
@MyScope //@1
- m" r3 w9 o! v, v2 I% apublic class User {- \# h& j1 n% m; B0 H

+ d, ]( w  Y) v    private String username;
% d- H' ~" H: W" t) n/ u, I" S# K' d" _- V  g
    public User() {
' d& A1 x4 a2 K: x* w        System.out.println("---------创建User对象" + this); //@2
2 [5 x( f& D( `* n" X8 ?% H! D* J        this.username = UUID.randomUUID().toString(); //@3
5 E0 Q% ?% I2 c' U4 Y    }- \4 @# |4 p& ?
2 ?7 u% Q, _, h7 s, d+ c& d8 A
    public String getUsername() {( A9 H9 a4 {8 q! u7 \# t3 I" V
        return username;/ }$ J5 D0 s  O. j, ?. r7 B, H
    }3 l" }) A6 ?  e. a' J4 [
5 u2 C, e9 N% f& k/ L) q8 M+ i
    public void setUsername(String username) {
7 b) @/ \% S& M. x        this.username = username;
+ C" R$ W  I9 ^* P" L4 E* c7 Y0 S    }
, @; v" F6 Y  H4 i! n$ e
+ D, b: [# Z* H5 b! @}: w7 V! ^% [5 o# O
@1:使用了自定义的作用域@MyScope; o/ D( ~5 G# D/ H2 ?8 s' v
* M! M/ `* P4 X+ _$ M' G& J
@2:构造函数中输出一行日志9 P( l1 s$ k* Q7 u) ]" z( t- w5 ?

& ^" A+ E1 v5 J; C@3:给username赋值,通过uuid随机生成了一个
7 z8 M. b& q' B
: O" @, ^! V% p7 |% f来个spring配置类,加载上面@Compontent标注的组件( q0 k$ o$ ?. ~

$ {) p* a4 f9 A/ F7 `. r1 spackage com.javacode2018.lesson002.demo18.test3;
) G1 Y7 i0 L0 e2 G
; L  ?1 U7 S; W" L/ b5 zimport org.springframework.context.annotation.ComponentScan;
3 f0 D1 G- Y+ Eimport org.springframework.context.annotation.Configuration;7 J3 |0 o$ x9 T; ~# R+ N
' d3 J+ S5 b& u) g" R. C
@ComponentScan6 J9 M. C6 g- T6 k$ l5 A
@Configuration
3 ^, ]4 s: i* g9 L# \public class MainConfig3 {8 F% V" O% G  O3 O/ p7 e
}# v1 c( y+ A/ A
下面重点来了,测试用例
; B) X* v! F( R- Q8 P4 v4 o
5 T: O& v' o3 M/ F0 D@Test! _3 N+ L# ^' a
public void test3() throws InterruptedException {
5 U. G4 G/ U9 D8 i" H2 v9 {6 S* h    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
+ h, S: a0 _( V- \; p  C    //将自定义作用域注册到spring容器中
- j- e2 n9 |" r" J" f; p. k6 P    context.getBeanFactory().registerScope(BeanMyScope.SCOPE_MY, new BeanMyScope());//@1
5 [/ B; q6 f' q  L. Z    context.register(MainConfig3.class);7 ^& i' I. S7 Z% K0 q6 `3 _& q
    context.refresh();
( M' \3 D0 J# h8 v' [" n& q5 w: m# O
: P! e4 u( d9 n5 d, x* g    System.out.println("从容器中获取User对象");
+ B) ^, [5 D/ ?9 I  n    User user = context.getBean(User.class); //@2# r1 U; w1 E+ A6 _" _" R& |; L; Y
    System.out.println("user对象的class为:" + user.getClass()); //@3; W5 c7 k; S6 ?' P8 X; Z: H' Q
0 e+ C. f3 ]4 l5 x1 i
    System.out.println("多次调用user的getUsername感受一下效果\n");
" q% h7 T# W) R0 a    for (int i = 1; i <= 3; i++) {
( Y* V. G# n) |6 `8 |) ]        System.out.println(String.format("********\n第%d次开始调用getUsername", i));/ s8 f5 z$ F6 C' i! Y
        System.out.println(user.getUsername());
! J3 B$ N' A$ U/ |* b# S: K$ Q! F4 g        System.out.println(String.format("第%d次调用getUsername结束\n********\n", i));
; K9 S; \! t9 x( G4 s. p    }! [. f) r9 l. v; ?5 R# a2 s. b; W
}" F& l; T5 V* V$ [$ J8 U
@1:将自定义作用域注册到spring容器中0 P1 ]$ j9 `" a2 L# }/ b
8 A$ C6 @; F9 |
@2:从容器中获取User对应的bean  G& \9 F+ \* D6 T* ]0 S8 Y* S

5 s2 X, K+ a& A' n( T@3:输出这个bean对应的class,一会认真看一下,这个类型是不是User类型的
! U8 {$ }* ^' ?  W  ?' n4 M5 O8 d% ^
代码后面又搞了3次循环,调用user的getUsername方法,并且方法前后分别输出了一行日志。
4 U. j) u+ f5 R, f# g% X9 R6 o8 `2 N# L4 F6 }
见证奇迹的时候到了,运行输出/ f# I* b: N  _% K+ ?
+ R; f0 z% m4 i- m8 Z$ `$ @+ r
从容器中获取User对象. p4 `; \- P  w( j& F
user对象的class为:class com.javacode2018.lesson002.demo18.test3.User$$EnhancerBySpringCGLIB$$80233127
( m4 {( a5 ?+ ^" u9 S+ L# O多次调用user的getUsername感受一下效果
/ i8 X% k; @- M8 g, d6 Z
2 e) C* S  i4 W; m********
, K- z' e9 m* O, y, g第1次开始调用getUsername
$ G" n/ f' m5 b! \1 f! ]$ cBeanMyScope >>>>>>>>> get:scopedTarget.user
( V& P! b, ]0 @' b2 M---------创建User对象com.javacode2018.lesson002.demo18.test3.User@6a370f4
: u" @1 m7 a* g1 f& @1 F% h7b41aa80-7569-4072-9d40-ec9bfb92f438
' l1 x+ e: O7 D2 ?第1次调用getUsername结束% }, K; {+ `6 Z2 F2 J# }8 u+ J
********0 k; |8 `! [: G$ ?5 w3 ^

1 u: b& ^: ?* H! F: {1 I********
9 R3 H2 H9 Y6 ]; @4 i第2次开始调用getUsername
0 B6 f/ D, c+ m+ BBeanMyScope >>>>>>>>> get:scopedTarget.user
. S" {) U. D( e4 a2 I+ Z---------创建User对象com.javacode2018.lesson002.demo18.test3.User@1613674b
$ ?. s6 X3 u7 ~2 k$ q* g01d67154-95f6-44bb-93ab-05a34abdf51f8 ^4 h; m# F! H, {' E) U
第2次调用getUsername结束
8 R' }! W, i$ t********
& a8 v, V- \2 M6 t3 w
1 u; H: w1 U( ~. X& E: g7 y3 O********- }# H! y9 E4 p1 M5 W
第3次开始调用getUsername
% }1 w! L* v; B; O3 }6 S/ [BeanMyScope >>>>>>>>> get:scopedTarget.user) Y# O6 G- [; B" O4 |
---------创建User对象com.javacode2018.lesson002.demo18.test3.User@27ff5d15
; L7 C3 V& j$ Z# v1 w) e! I76d0e86f-8331-4303-aac7-4acce0b258b8' i% L+ \( _; s: _. _
第3次调用getUsername结束
4 i  w9 q( H( \; K********
/ r* B- O1 T/ Z3 h$ I1 {. n从输出的前2行可以看出:* w/ A0 \, f# c7 A! k
' O" N7 T2 q4 H$ A
调用context.getBean(User.class)从容器中获取bean的时候,此时并没有调用User的构造函数去创建User对象, C* V, t) U; _- N
$ C0 p/ @( ?- H3 \3 V9 E$ T
第二行输出的类型可以看出,getBean返回的user对象是一个cglib代理对象。
: X$ O2 p2 K4 T. r! B$ c/ d& r) ^3 y$ `+ O. c
后面的日志输出可以看出,每次调用user.getUsername方法的时候,内部自动调用了BeanMyScope#get 方法和 User的构造函数。
! j1 T  C) t3 l. d( j5 s4 X" @/ E* C! n6 P% W+ [2 z
通过上面的案例可以看出,当自定义的Scope中proxyMode=ScopedProxyMode.TARGET_CLASS的时候,会给这个bean创建一个代理对象,调用代理对象的任何方法,都会调用这个自定义的作用域实现类(上面的BeanMyScope)中get方法来重新来获取这个bean对象。: h6 e( ^5 G) W; F8 Q# w

. M7 `/ V* Z5 q7 @动态刷新@Value具体实现& \/ K6 a$ w$ a! }) t  B  p

% X8 l( M  g+ R/ U* q' A4 X那么我们可以利用上面讲解的这种特性来实现@Value的动态刷新,可以实现一个自定义的Scope,这个自定义的Scope支持@Value注解自动刷新,需要使用@Value注解自动刷新的类上面可以标注这个自定义的注解,当配置修改的时候,调用这些bean的任意方法的时候,就让spring重启初始化一下这个bean,这个思路就可以实现了,下面我们来写代码。
. J9 [+ B% c2 K6 u/ n
. S( a7 @! I8 B/ p. X: O3 L先来自定义一个Scope:RefreshScope
" H/ {7 m( T0 u- j/ z. h' G3 ~5 u1 O
) a* V) [; \) U% Tpackage com.javacode2018.lesson002.demo18.test4;
( v: w. \7 J8 d0 r
" _' l$ i3 G  Wimport org.springframework.context.annotation.Scope;5 r& B5 \+ ]8 L' v" W' A+ H' ^
import org.springframework.context.annotation.ScopedProxyMode;
0 ^: V' y, l1 t( C) T
  k3 U5 P7 |; iimport java.lang.annotation.*;$ C8 B$ C( r  u0 \" |
6 M+ m4 B! Z& f2 [9 y
@Target({ElementType.TYPE, ElementType.METHOD})
' S0 v1 X- F" i4 k+ V4 r@Retention(RetentionPolicy.RUNTIME); V; E- s& z' ]' D" l
@Scope(BeanRefreshScope.SCOPE_REFRESH), c2 f6 R# t; P6 a
@Documented
. w' y6 k" b" ?  gpublic @interface RefreshScope {$ N& z# ?4 l" v0 d( H" A
    ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS; //@15 b5 ~" h" f( R4 \, x
}
& E; c( {! R  z+ a9 f# l要求标注@RefreshScope注解的类支持动态刷新@Value的配置
7 m0 o* [7 H8 }9 B5 i$ k
  x& j. a$ Z# Y+ Q4 a0 ]( B5 R@1:这个地方是个关键,使用的是ScopedProxyMode.TARGET_CLASS* Q6 z  K% C  G4 h$ B' F0 n0 X

0 L- ~& s! c+ o( ?5 \& i1 Y$ h这个自定义Scope对应的解析类: a5 q3 n) a9 [) D+ E3 y4 D* z, V
! V  @- W  n8 T% ]/ g
下面类中有几个无关的方法去掉了,可以忽略
3 U; p3 ^+ _' i
" h6 n6 F; i; @6 d6 w2 }package com.javacode2018.lesson002.demo18.test4;
+ y  C( I! i8 V) f6 X, c: a1 A7 x. s! p' b' P' t
4 P; m0 T- ^7 _. Q) G$ m& W
import org.springframework.beans.factory.ObjectFactory;( A/ a' G+ X0 x3 m  Q# t# r/ ~! t
import org.springframework.beans.factory.config.Scope;3 Z# v2 ~' i7 e& {) A: G( m, X
import org.springframework.lang.Nullable;
. g- Q: W& g& x9 s9 O0 B/ o; g8 ]  l" g" W
import java.util.concurrent.ConcurrentHashMap;
/ u0 j) {' C7 o: I# d4 P
2 P- u: Q% K; Ypublic class BeanRefreshScope implements Scope {
( n# X; ^2 r' m. _# E7 B8 K6 V- |8 c- Y& L4 w
    public static final String SCOPE_REFRESH = "refresh";
" W4 I& i9 n" Z& I) ?5 f$ G1 Y
5 O3 L5 d1 _$ S" y0 K    private static final BeanRefreshScope INSTANCE = new BeanRefreshScope();
0 t! o4 w3 ~) F6 h, Y" O
" {5 p9 P% {# s( e0 z' h    //来个map用来缓存bean& K' |. b7 U% m8 @3 N8 }
    private ConcurrentHashMap<String, Object> beanMap = new ConcurrentHashMap<>(); //@1
% V* ]# m! s& c4 V5 V# z: J$ K0 d0 ^. t9 Q$ K: {) p0 G
    private BeanRefreshScope() {  P- h& [8 x5 U9 ^
    }+ z! W/ ?2 c, }6 p: ?# s
# m7 x/ `9 e: b1 n0 \
    public static BeanRefreshScope getInstance() {% I3 _! `' K1 r8 Q; C* k3 F
        return INSTANCE;
! u" X3 O8 N* M0 S! f) M    }4 O* s* m0 }5 B8 k" @) R# O6 O

' Q. L9 p* \* W, q. |" ~- ]. e* d! ]    /**% o( b7 I4 F! r) c7 q. ?  n
     * 清理当前. n2 [2 v* s# m! M
     */, \2 R+ [- N3 t8 a% N
    public static void clean() {
% l8 T1 m* ~4 H# i6 k        INSTANCE.beanMap.clear();9 O; m" W9 h: @- A
    }
3 E8 o+ D5 l; w# Y+ T9 m) q. l1 `: t* e; c) T2 P' A5 m1 s0 n
    @Override$ s3 E- }) E' }- g4 p  X( q- e- w: v* A
    public Object get(String name, ObjectFactory<?> objectFactory) {0 r$ B3 r6 @+ \& J: L
        Object bean = beanMap.get(name);( T) F0 M( J( o  i  l1 \' K
        if (bean == null) {
( q& D/ q* S3 a            bean = objectFactory.getObject();, }8 r! A# }8 x9 h* K+ I6 c6 l
            beanMap.put(name, bean);9 o; D0 k1 _% R. Q; W0 @/ l$ ]$ k
        }
% ~8 h  B' h. J# C; Q9 Q        return bean;7 G9 i/ ?; i; a  w! c
    }% L9 b! ~8 k# d% J( e
& r/ E" ~$ `' `+ B" o
}
0 d2 Z6 V! F1 j( o上面的get方法会先从beanMap中获取,获取不到会调用objectFactory的getObject让spring创建bean的实例,然后丢到beanMap中6 l  ]+ e6 Y/ h8 B+ v8 V8 R

4 ^5 ?$ w( ^1 f& \上面的clean方法用来清理beanMap中当前已缓存的所有bean* V8 q' I: m8 X0 M1 K4 J1 Y
, |* {, U- F3 U: i
来个邮件配置类,使用@Value注解注入配置,这个bean作用域为自定义的@RefreshScope. x+ t- O9 g' u4 f' e- ~
% Y$ E# e* J- A; `6 N( x
package com.javacode2018.lesson002.demo18.test4;
' {: ~9 s3 C: c: E$ X9 x/ x# M8 I' v
import org.springframework.beans.factory.annotation.Value;
1 k1 `- g0 w2 W7 q/ k* u2 kimport org.springframework.stereotype.Component;% ~7 d) C6 W0 _# Q4 ]/ r! ~8 m0 Y0 u
! b' F  S2 r# D- H6 a5 ~$ V
/**3 N) l7 d/ k3 M- x$ N
* 邮件配置信息
( N) k; j2 ]" L% T */
9 ]% {& S6 a% B' W2 ^@Component( V9 n5 e) F$ t7 v; M* {
@RefreshScope //@1
5 [) b8 C) n+ n& x: u/ E7 tpublic class MailConfig {+ D* t( s  c, Y" u# T5 \$ I4 W0 N

5 Z/ y& {) |) L8 S1 R    @Value("${mail.username}") //@2. H* V) F/ e8 }0 N$ [. Y7 ^
    private String username;. k, ~0 s1 `% Z
2 q$ R3 c6 e# |: {
    public String getUsername() {: [: n! P! V# y
        return username;
5 H5 `7 B6 r& ]! V5 {" W/ N    }3 ?2 g  ]2 n. v( A. h# y2 I& I3 H
4 A5 t$ Q! S7 Y; f$ @3 d
    public void setUsername(String username) {4 |$ [& K# W6 \) y% @$ J4 G
        this.username = username;
( S. v( T# d0 {# N) p0 ^/ T+ F    }
. v; c% a" \% Y: U4 x/ j% B) i  c& n. S- X
    @Override
- m$ V+ w  ^7 B- b& O    public String toString() {5 ^: w4 U# q% r: l6 f  }
        return "MailConfig{" +" q( M" L# h3 [% h0 P1 K# x' L7 |
                "username='" + username + '\'' +
' D1 k- v  l/ T1 X) b  C' m6 [                '}';
' @3 Y: m2 O5 R; a% C6 F* W+ e1 Q    }
, G  y+ O: r- w}/ k" Y; Z/ _7 Q3 l( s( H/ o7 O
@1:使用了自定义的作用域@RefreshScope2 l% Y& U( r6 a( _" _8 S

5 T! G5 @  g8 e4 x5 f0 `@2:通过@Value注入mail.username对一个的值/ q: i9 v# L- Y1 A+ y

% F0 E  l: C) ?4 Q5 R重写了toString方法,一会测试时候可以看效果。
1 Y/ Y+ F5 {$ y9 K5 X+ n2 W, v, Y9 \% k, m. u: x, z8 I
再来个普通的bean,内部会注入MailConfig
& F5 i; l( J: Q
( D, G- J* l1 s+ X7 {& }$ T; lpackage com.javacode2018.lesson002.demo18.test4;
2 s3 b4 Q/ W4 R4 F2 B4 I6 g6 |; O! P8 @7 k
import org.springframework.beans.factory.annotation.Autowired;
' _9 A, J6 ~8 z8 U3 @& Y1 X* Pimport org.springframework.stereotype.Component;% d* Z7 a7 Z* i
6 x3 ]' p9 |5 a* Y9 j! Y
@Component) m5 V2 ~" \2 d' U% }& D9 B8 I' w
public class MailService {
# T+ W  h+ p6 ?+ Y! a, {* H    @Autowired
$ @6 D- B1 G: \& V) X* m    private MailConfig mailConfig;
! q8 u* ?4 W% Z2 L
2 Q7 \* F6 e& W# h    @Override
% Y; z9 U+ H  r) V0 x" R    public String toString() {
  h# u3 e' h. @+ ~7 i" _        return "MailService{" +
. H3 X" F: {& s9 a                "mailConfig=" + mailConfig +  ~9 a# }/ P) V9 [
                '}';4 ^- V5 u- z* F5 [
    }
9 c  O- s9 ?& X  ?; c$ [) H}
+ O/ C- r0 G$ U9 [8 \+ p代码比较简单,重写了toString方法,一会测试时候可以看效果。5 k4 B  T, b6 S4 A1 A
+ V2 S% n- e* s, {% j( G
来个类,用来从db中获取邮件配置信息. ~. N1 I) b& u& q

4 s$ Y8 b7 J8 w: M* h4 l$ gpackage com.javacode2018.lesson002.demo18.test4;
$ W7 i; X8 z5 e; x" i; d( x2 s* S/ U; c7 Q. v3 e) K! n5 G
import java.util.HashMap;
9 u6 E* n. I+ K8 @% y- L7 b+ g7 ~import java.util.Map;' G, H4 N+ W9 \# U+ f8 ]
import java.util.UUID;2 u' {% B3 @2 R. ~& G7 _; R) L

+ d# }0 o5 _4 e7 E$ P# d/ hpublic class DbUtil {9 X! P( S; |6 }) b; _
    /**
! |7 x9 }; N( E     * 模拟从db中获取邮件配置信息
+ Y9 p6 h  K3 A0 o     *
: i9 g# J: H3 Q# r; Y. c% l     * @return. r  y2 O& G0 z4 g
     */
$ t, W% u. z  U; W4 y! M3 z    public static Map<String, Object> getMailInfoFromDb() {
3 F. z: ?9 X7 G: z        Map<String, Object> result = new HashMap<>();9 ]/ q3 C, I; a5 `
        result.put("mail.username", UUID.randomUUID().toString());( k: m, V/ v6 d" V3 ?
        return result;1 J. \6 x/ P7 D: ~8 `2 E* r, x) j
    }
* U3 B0 @$ _" x}5 n. P: C: K3 C5 Q! [
来个spring配置类,扫描加载上面的组件
8 o( `! ?3 {0 d) s4 z3 ~# g3 s
package com.javacode2018.lesson002.demo18.test4;+ D* U& x" H9 g8 K
0 f& ]' F0 j4 Y+ j" j* x9 d
import org.springframework.context.annotation.ComponentScan;
! N' N$ U; F8 R. a3 ^: j9 ?import org.springframework.context.annotation.Configuration;
1 L5 O& H5 a  v  N8 a& n
& D% a$ k* ]+ t# V% |* {3 w$ P@Configuration7 l+ I2 ~5 ]2 O! C. W2 ~4 C* ]
@ComponentScan, Q& `( ^) {* N) e, u' h
public class MainConfig4 {. F+ _' I) d0 t6 c6 B
}
+ T2 X; l8 N! p) I来个工具类) ?8 ]6 @) C/ I" p

1 M7 x& B+ O, ]内部有2个方法,如下:6 T$ y3 Q6 J2 `/ R
. c7 z1 T: J6 O& c+ _: t
package com.javacode2018.lesson002.demo18.test4;
# h3 q) Z! M* X
# z2 t! i/ |0 q, z9 aimport org.springframework.context.support.AbstractApplicationContext;! G) C! ~! h, C5 X8 T# ~) r
import org.springframework.core.env.MapPropertySource;- u0 o- V8 p$ A1 p+ x/ R
) K, ^/ D9 w  p# b
import java.util.Map;/ t6 T9 }; D' f

( d8 O0 W& _) i! n) opublic class RefreshConfigUtil {; C$ a0 b$ K) P& I* g8 W' L$ z
    /**
; r: @9 K3 j( u. y  w     * 模拟改变数据库中都配置信息7 J! F, W( g. [' p3 R; V" B
     */* q- r2 B$ R4 w; s1 \) w( D
    public static void updateDbConfig(AbstractApplicationContext context) {' G5 K6 P: P4 O* w) C
        //更新context中的mailPropertySource配置信息. N: h" q% @8 n2 ?. h- U. I
        refreshMailPropertySource(context);$ a: _: z. g( {) B% }3 V# Z( @
' @, m4 B. I* t5 S" d
        //清空BeanRefreshScope中所有bean的缓存
1 _8 l( b/ m8 p0 u  f        BeanRefreshScope.getInstance().clean();* N$ ~, N+ r- h/ [& Z
    }
6 \/ X+ I) r- N5 K! w" }( [6 n2 @0 ~
    public static void refreshMailPropertySource(AbstractApplicationContext context) {
: A, u' }6 E4 w0 c        Map<String, Object> mailInfoFromDb = DbUtil.getMailInfoFromDb();
" F( j3 v" s  M  j        //将其丢在MapPropertySource中(MapPropertySource类是spring提供的一个类,是PropertySource的子类)7 A2 o3 f8 y' h& Q. m. `
        MapPropertySource mailPropertySource = new MapPropertySource("mail", mailInfoFromDb);
/ X) y0 C5 \5 t  V8 Q        context.getEnvironment().getPropertySources().addFirst(mailPropertySource);
1 ]( v. Q: f7 U! S- _* {- v$ v4 x    }
3 C( I+ |: \) j+ G, i% h
: ~) C9 A( t, R}6 l' r( O! u$ Q7 h
updateDbConfig方法模拟修改db中配置的时候需要调用的方法,方法中2行代码,第一行代码调用refreshMailPropertySource方法修改容器中邮件的配置信息2 Q. w- e! \% g* @' k

% _  O, K! L  ~: ^; [BeanRefreshScope.getInstance().clean()用来清除BeanRefreshScope中所有已经缓存的bean,那么调用bean的任意方法的时候,会重新出发spring容器来创建bean,spring容器重新创建bean的时候,会重新解析@Value的信息,此时容器中的邮件配置信息是新的,所以@Value注入的信息也是新的。
, W+ n( f) M( C6 r' g
/ g' m( ^# G- I9 {; i3 j来个测试用例9 J  |2 q* u- l! l, q- P2 {
9 M9 W: Q  B2 X" E! S3 t% {
@Test
; Y; @6 n" k( _/ ~6 P9 Zpublic void test4() throws InterruptedException {1 H7 y! J6 |7 F& C' O
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();! Z5 _7 G  M$ `) s, D  W
    context.getBeanFactory().registerScope(BeanRefreshScope.SCOPE_REFRESH, BeanRefreshScope.getInstance());# ~6 g6 C  @- _. I, M9 v: f2 z
    context.register(MainConfig4.class);
# g% i& w/ y) T4 a# L& N  Z" H    //刷新mail的配置到Environment2 w1 z) E4 U: T& q* y
    RefreshConfigUtil.refreshMailPropertySource(context);, U- r7 U7 L" J: X4 F
    context.refresh();$ n! G4 I: p1 M1 U* ]/ c( O( Y

! m# F+ k! n8 _    MailService mailService = context.getBean(MailService.class);
; a8 N, O8 R8 d" F- d0 |    System.out.println("配置未更新的情况下,输出3次");
2 K" }  V' s5 m# K0 P6 q& N    for (int i = 0; i < 3; i++) { //@1/ ^) K" k3 e+ Y+ T; V* @6 E& D
        System.out.println(mailService);9 E3 A" w7 V7 g5 P$ A7 Q$ p! h
        TimeUnit.MILLISECONDS.sleep(200);( c8 B% c! O. C! u9 i
    }
, p/ j2 K6 _6 k* b" w) U* c3 z4 V1 ^0 B5 Q9 }0 q
    System.out.println("模拟3次更新配置效果");! q1 Q* w5 I; ^8 z
    for (int i = 0; i < 3; i++) { //@2
1 Q+ h( b: n8 P1 n        RefreshConfigUtil.updateDbConfig(context); //@3# J$ P! {$ T% R9 ~& J
        System.out.println(mailService);" ?/ ^2 \+ A8 \2 V, i' O1 T! e
        TimeUnit.MILLISECONDS.sleep(200);
( C  d% O8 g2 |    }* k. C: `( k1 `/ q
}3 w: D; Z2 I+ j! x' w  a
@1:循环3次,输出mailService的信息
3 |# R/ C) X" j4 a  ]. ^' a; a( j7 C: \. t( o* C) w- ^! ]
@2:循环3次,内部先通过@3来模拟更新db中配置信息,然后在输出mailService信息
1 {! C; _, a7 |; Q& o
" G- D; o- ~8 W' @见证奇迹的时刻,来看效果
9 q6 G6 x! y' w
( M) X3 p% _  |" i6 U( o配置未更新的情况下,输出3次/ ~/ Y4 B: K8 |9 N5 Q
MailService{mailConfig=MailConfig{username='df321543-8ca7-4563-993a-bd64cbf50d53'}}
6 Q" i5 _/ O% v. Y6 c5 }: s9 MMailService{mailConfig=MailConfig{username='df321543-8ca7-4563-993a-bd64cbf50d53'}}$ i% A5 O2 c; U; X* o( V
MailService{mailConfig=MailConfig{username='df321543-8ca7-4563-993a-bd64cbf50d53'}}7 B9 P* F( R, i* E- j
模拟3次更新配置效果
. n' i+ |5 {& `$ w* O0 p/ n& HMailService{mailConfig=MailConfig{username='6bab8cea-9f4f-497d-a23a-92f15d0d6e34'}}
' j, ?* y7 ?) B* cMailService{mailConfig=MailConfig{username='581bf395-f6b8-4b87-84e6-83d3c7342ca2'}}
9 [# f/ L; _+ ?7 JMailService{mailConfig=MailConfig{username='db337f54-20b0-4726-9e55-328530af6999'}}2 B- A% ?4 H' p6 H- S
上面MailService输出了6次,前3次username的值都是一样的,后面3次username的值不一样了,说明修改配置起效了。# }9 E) c: m  y0 _; y

  d& a8 C2 @& e7 I小结
% z2 t+ \. B8 J- C
& J/ h# I, ?  T# _8 W$ w1 q+ ?4 }动态@Value实现的关键是@Scope中proxyMode参数,值为ScopedProxyMode.DEFAULT,会生成一个代理,通过这个代理来实现@Value动态刷新的效果,这个地方是关键。% B' X2 y) H- d+ H
; `& s3 n) H5 J, R9 O# Y
有兴趣的可以去看一下springboot中的@RefreshScope注解源码,和我们上面自定义的@RefreshScope类似,实现原理类似的。
1 E6 x$ e- M0 t: w. `: ^/ F. m* f5 z5 a7 \$ o) I8 U/ X5 V
总结+ D9 e3 }1 _4 X' p. c, P2 H- d

3 \% k4 ]/ W% E) @本次面试过程中3个问题,我们都搞定了,希望你也已经掌握了,有问题的欢迎给我留言,交流!
, x+ L) X2 N' X. o, j! q
" a/ g3 K6 z+ [0 D* D案例源码/ a, N5 ~' e7 A( ?

% e2 ]' S- D$ C4 L7 j* G  whttps://gitee.com/javacode2018/spring-series
* N" o: t$ Z8 V3 p. B路人甲java所有案例代码以后都会放到这个上面,大家watch一下,可以持续关注动态。% K8 W9 Q5 f- P) g0 g; j
& V5 ?0 r0 P# g" l" v8 `
Spring系列
" K: L; P3 g9 H" |  V, q  V, b* ]& [( ]! J
Spring系列第1篇:为何要学spring?
3 j& Q1 Z7 M3 _+ S# d; L, m* n5 N0 Y# q' I3 q
Spring系列第2篇:控制反转(IoC)与依赖注入(DI)
+ K1 ?' D: C  }6 B/ `# f, z0 y: c- b7 f
Spring系列第3篇:Spring容器基本使用及原理
; R& L6 i% D' l; k& @; c. T
! Y' J% e3 @5 }' uSpring系列第4篇:xml中bean定义详解(-)5 _9 j0 t/ F. p0 u/ k

5 A  U( F7 l0 A" iSpring系列第5篇:创建bean实例这些方式你们都知道?
7 V7 f. r( c7 `; p4 ]/ w2 E# r+ _
: j. Q  S3 p9 R: j) w. z' VSpring系列第6篇:玩转bean scope,避免跳坑里!
6 c) u! }% J' K4 |- s4 g
& ^2 v; V; P: ?  l9 o* JSpring系列第7篇:依赖注入之手动注入% S/ T1 q' p% s. }6 e" w
# f0 B; P* {+ i: ?: Y
Spring系列第8篇:自动注入(autowire)详解,高手在于坚持) y/ T2 L0 f& E3 Z

- i% P6 K3 ^, T) o7 u7 RSpring系列第9篇:depend-on到底是干什么的?
1 X. E: {1 `; ]
  d6 e+ Q( K$ a1 kSpring系列第10篇:primary可以解决什么问题?" o: p; {) S+ Y

+ y: |% {* A( P- v1 T+ F% t! f* Q$ Y9 a# pSpring系列第11篇:bean中的autowire-candidate又是干什么的?8 v) \/ F& M5 N
, D; R* Z4 C( f; z" `
Spring系列第12篇:lazy-init:bean延迟初始化1 `8 i- Q% }1 {5 y- h7 o
5 [1 H+ t- t6 b% |. y
Spring系列第13篇:使用继承简化bean配置(abstract & parent)
# B7 s% P/ j  L
8 {5 H7 H3 d3 A; WSpring系列第14篇:lookup-method和replaced-method比较陌生,怎么玩的?
0 I5 m0 k7 R- n& J2 M/ }$ `
* b( d/ R. R$ B4 d+ }Spring系列第15篇:代理详解(Java动态代理&cglib代理)?
" |& J# b- A9 [% R8 l* y
' w7 W5 i4 E: H, Z0 X. oSpring系列第16篇:深入理解java注解及spring对注解的增强(预备知识)
+ G* m$ K8 e6 Z# ~. @2 C
6 r7 ?3 |" R' U* O. Z$ USpring系列第17篇:@Configration和@Bean注解详解(bean批量注册)
" W+ n4 o% i( w
# o; U6 L7 \8 @9 A3 oSpring系列第18篇:@ComponentScan、@ComponentScans详解(bean批量注册)
! O  z( W6 n" g- V2 s) @# X4 J7 x& |7 C  G& ?0 I0 {/ E
Spring系列第18篇:@import详解(bean批量注册), O5 i" J- Q! l9 c8 V: i9 q
: g$ \8 S7 [8 N+ s6 N$ K4 j
Spring系列第20篇:@Conditional通过条件来控制bean的注册
$ E5 A% x* R9 {" F% b7 @5 S6 R2 l- I4 J- p
Spring系列第21篇:注解实现依赖注入(@Autowired、@Resource、@Primary、@Qulifier)
! }2 Z: t- Z4 C- W% i+ w  H2 H* _
Spring系列第22篇:@Scope、@DependsOn、@ImportResource、@Lazy 详解! m0 ^6 v: e# b1 s, `
( ?1 _) D. W( a8 V
Spring系列第23篇:Bean生命周期详解
) h' o7 \. D0 B/ Y! K" U
2 ]3 o/ h+ f0 ~Spring系列第24篇:父子容器详解
9 n5 L4 m# ]8 r, @" w4 q2 D# y; m) A
! P2 w9 z% J( u( n- N更多好文章3 \  p& h2 ~. S1 n$ q
& e. z0 y: V2 K. n+ V" Y! \, Y8 e
Java高并发系列(共34篇)
: x2 f3 q1 ]. s1 r4 M
/ }. _# m. k& t9 [6 N5 `6 ]MySql高手系列(共27篇)
5 k" Z/ q$ O; n. N/ K
3 j* g  |5 L7 D9 u2 QMaven高手系列(共10篇)& q# G) t* v8 ~) P5 Q6 U7 E

) C* O, f) @5 P0 Y% I$ w% JMybatis系列(共12篇)% |  h1 D5 \2 i

) L+ I0 {9 X1 y. ?2 n& U+ a聊聊db和缓存一致性常见的实现方式( L. q( H/ [: E

% {9 l9 Z3 j* f% r4 W& o3 R接口幂等性这么重要,它是什么?怎么实现?1 o! r: e; z6 V& `% `' j1 s

) q" L( b/ u& d' _2 F3 E泛型,有点难度,会让很多人懵逼,那是因为你没有看这篇文章!
8 z8 h2 m  L  ?/ a6 V' \2 u, C————————————————
( R3 y- F# w& h5 C. N版权声明:本文为CSDN博主「路人甲Java」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
% r* t9 q$ }# f8 ]原文链接:https://blog.csdn.net/likun557/article/details/1056487578 `6 v6 w7 d! Q" m8 i7 R

3 A# t% B, l* O2 q0 G
0 ~2 U' b9 }. _- Q5 A




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