数学建模社区-数学中国

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

作者: 杨利霞    时间: 2020-5-23 11:01
标题: 太狠了,疫情期间面试,一个问题砍了我5000!
太狠了,疫情期间面试,一个问题砍了我5000!" e. x  v( I( R4 j
疫情期间找工作确实有点难度,想拿到满意的薪资,确实要点实力啊!6 {8 _8 q+ }  |' P: k  K0 C. A
3 m: g1 w6 q. v
面试官:Spring中的@Value用过么,介绍一下
  y: q! `) Q& F! l* f
" V+ V4 n  X2 O$ J% @* I: X% g( W$ ^; t我:@Value可以标注在字段上面,可以将外部配置文件中的数据,比如可以将数据库的一些配置信息放在配置文件中,然后通过@Value的方式将其注入到bean的一些字段中
0 H- C. x5 r  r1 @. S9 w' M5 r; M) E. c
面试官:那就是说@Value的数据来源于配置文件了?0 j" P4 A+ K& L, k8 n
9 h, R: p" G* |  {( n
我:嗯,我们项目最常用更多就是通过@Value来引用Properties文件中的配置
7 J# M2 l/ y1 V9 {
1 M6 A, Q4 v# w. t' a面试官:@Value数据来源还有其他方式么?. K$ w- s$ J1 {+ N

# A  X5 ~6 z0 F我:此时我异常开心,刚好问的我都研究过,我说:当然有,可以将配置信息放在db或者其他存储介质中,容器启动的时候,可以将这些信息加载到Environment中,@Value中应用的值最终是通过Environment来解析的,所以只需要扩展一下Environment就可以实现了。
3 h) f8 z5 L$ }! h; M* C+ ]! L
$ n7 P  }3 I5 n: c0 c& {0 ^面试官:不错嘛,看来你对spring研究的还是可以,是不是喜欢研究spring源码?3 |) G* r& L7 j  H/ e, N+ p/ N

5 N0 ~6 \; y: r我:笑着说,嗯,平时有空的时候确实喜欢捣鼓捣鼓源码,感觉自己对spring了解的还可以,不能算精通,也算是半精通吧. U# a$ m0 {# q; n" H

4 W$ U( m0 W9 u' w$ y面试官:看着我笑了笑,那@Value的注入的值可以动态刷新么?3 J. l! }1 w2 s" A5 t

! d# P9 j: K" D* O. q5 H我:应该可以吧,我记得springboot中有个@RefreshScope注解就可以实现你说的这个功能9 {* I, h* W$ _  W/ h# P' @
7 n/ s, h/ G4 G
面试官:那你可以说一下@RefreshScope是如何实现的么,可以大概介绍一下?0 S" G* s3 y, J/ o& x; z
3 x+ e9 k. e0 y
我:嗯。。。这个之前看过一点,不过没有看懂
4 E4 g" r7 A: A7 \8 @4 S# n$ V2 \! q: }+ ?# d- ~( B
面试官:没关系,你可以回去了再研究一下;你期望工资多少?+ E# c+ o/ Q5 }! M- ?/ ~1 T9 ^' i

$ ?$ v& i2 F5 F5 m& I5 v, p我:3万吧# n9 T2 [1 m8 v

0 O2 e/ h* {9 W% m面试官:今天的面试还算是可以的,不过如果@RefreshScope能回答上来就更好了,这块是个加分项,不过也确实有点难度,2.5万如何?
' E5 d- i2 v2 V4 t( P$ `) j' V/ U8 E( ]8 y" B- w8 K/ g
我:(心中默默想了想:2.5万,就是一个问题没有回答好,砍了5000,有点狠啊,我要回去再研究研究,3万肯定是没问题的),我说:最低2.9万  q( ?" {$ a* g7 ?$ g  Z9 }2 P
. L/ b7 C2 O( |, Z6 X
面试官:那谢谢你,今天面试就到这里,出门右拐,不送!! ?. h( a* g! `* R7 }. i) i
0 s- F5 U& y7 b+ b# d' b- g
我有个好习惯,每次面试回去之后,都会进行复盘,把没有搞定的问题一定要想办法搞定,这样才不虚。( h' X+ {' |# x: p& s2 e( Y) u5 G
0 k& H9 k" {" n  h
这次面试问题如下
/ e0 t# N9 ?7 \: M! W! x4 k: Y  S
@Value的用法
& E' _! l0 G8 _+ E1 N, P2 D9 X' n( j% m
@Value数据来源# X* |, \' H# F3 S: W9 o
* a; c6 U) V% K% \
@Value动态刷新的问题
1 B- q, w+ I8 u  c0 Q  s, z
7 U( z) V3 @5 a. W+ q# f下面我们一个个来整理一下,将这几个问题搞定,助大家在疫情期间面试能够过关斩将,拿高薪。6 b3 W. W( V6 n3 q4 V& b8 h

* e- l) o' `* K! ]# b7 B@Value的用法( ~3 h$ Y' b) D! v

' S6 x! b- j0 K系统中需要连接db,连接db有很多配置信息。
% f3 Z0 A$ y7 l! B3 m. L
! ?& n6 N' B. B1 U9 X# h* X系统中需要发送邮件,发送邮件需要配置邮件服务器的信息。
* e' M. |: P2 }6 N! ~& z9 G0 k% |+ X4 _& r6 J) f& k# B: R4 O
还有其他的一些配置信息。
" f* x! t% v2 j3 v( z1 D  h$ F3 i+ w! j, U" H! |
我们可以将这些配置信息统一放在一个配置文件中,上线的时候由运维统一修改。
- ]$ F: u' ^+ E4 a: q$ U2 E5 c. U* u, l8 {
那么系统中如何使用这些配置信息呢,spring中提供了@Value注解来解决这个问题。7 d; R6 V$ t" X9 A# [3 J
1 |( g9 n1 D! P. G/ _4 b* j
通常我们会将配置信息以key=value的形式存储在properties配置文件中。3 [+ a, t  c$ q1 S, @
. n7 S$ S$ x) e# g. J1 Q
通过@Value("${配置文件中的key}")来引用指定的key对应的value。
5 \' }% u2 p: h: |! G" `' \$ C2 M' e; f+ H. p: N" j% C
@Value使用步骤
( d! \  A4 w; o; ]$ b8 T- Q! x" `' L8 G6 z! e' `
步骤一:使用@PropertySource注解引入配置文件
+ t, m" M% |: _5 {* j, y! R) Z9 d- V% V
将@PropertySource放在类上面,如下
  [5 g, b- X# E6 I5 L5 a! ~: g' H5 H7 ]
@PropertySource({"配置文件路径1","配置文件路径2"...})
0 C4 a6 A# U( ~2 Z6 z* {% z@PropertySource注解有个value属性,字符串数组类型,可以用来指定多个配置文件的路径。
# k0 k( Y! Y0 S3 d. [" o: I4 |( L, G* _& W$ {
如:! U* p8 m5 o9 u! d4 L

1 ~' x. @) |# z( P. B- x3 Y4 I. W@Component# I& R" s3 t- z/ Q  C  y
@PropertySource({"classpath:com/javacode2018/lesson002/demo18/db.properties"})
: M3 \3 a% |4 opublic class DbConfig {$ {* }5 j+ \2 {  m) J
}) j, Y5 `% m  K- q, }6 ]) f
步骤二:使用@Value注解引用配置文件的值
6 O  \' J# J" Q) p  H5 @( ]3 v6 w& a' ]4 m. t
通过@Value引用上面配置文件中的值:8 P* N- Q, K' ^6 N/ {) m0 U% s8 C

  V2 O2 ]6 z1 ~  I( {" p3 w' o语法' h, |8 W* h# ^" s
7 F2 a9 N2 F5 z5 v/ T3 ~
@Value("${配置文件中的key:默认值}")' [" t4 L: ?+ b, l7 w) {* ^1 k
@Value("${配置文件中的key}")1 J) B; G8 e: ~  a( [2 q4 u0 b, ?: p/ ?
如:8 d) z9 h4 ]0 J* C8 w4 H+ {
8 a3 Q; `' A8 s1 r0 z1 `8 [; Z
@Value("${password:123}")
9 I. i0 m  B, g7 Y. L% H9 A上面如果password不存在,将123作为值4 e; H  [2 L" R" I3 r
: {) @5 `! D; ]% W
@Value("${password}")
/ i8 A5 `, H9 S$ D! i上面如果password不存在,值为${password}
$ l* B; v( e2 u6 e* K+ L/ H0 Q$ X; c- K9 y9 Y! Y- B
假如配置文件如下
+ T- m: y- i5 s  B
8 m, [* {  a# b: P  Ajdbc.url=jdbc:mysql://localhost:3306/javacode2018?characterEncoding=UTF-8
2 v& D( M" }3 `1 m9 y+ xjdbc.username=javacode
7 j* o# i8 E% u9 P7 \1 ?3 g+ ^jdbc.password=javacode0 G9 f- z8 B9 [2 H. Z2 T
使用方式如下:' Z# m  P* Z  v

# W* A; f& I+ H; E* ~9 f% U* b@Value("${jdbc.url}")! w  Z/ z0 W0 }* x( ~/ I- S
private String url;% {) h8 v, g( d

6 z. s, r) q5 g@Value("${jdbc.username}")# ~+ D$ |0 y5 l7 X2 U
private String username;/ n! E, s# r+ [

/ U, g+ Q8 T+ V0 T8 a@Value("${jdbc.password}")
! I6 J& k6 ~- s; k% k) U2 [private String password;8 H8 R$ X& b, y9 w* \
下面来看案例
( f( u" b2 b9 `% o
; }- \2 D" o# \1 k5 X, A- ?3 O) l案例
5 j# ^, b8 R  C/ |" m) o8 x/ G& R; ?* S) F7 {) O# z: G
来个配置文件db.properties
; V' _* V3 Q7 Y% S) P2 N0 C, e/ P. p) e/ k. t
jdbc.url=jdbc:mysql://localhost:3306/javacode2018?characterEncoding=UTF-8) ?$ O# `1 m; S* V- l
jdbc.username=javacode  C* x3 h9 [7 e6 }# D$ y
jdbc.password=javacode; d0 C: ]( L/ @; o: n# @
来个配置类,使用@PropertySource引入上面的配置文件5 F7 f0 h1 q  W' s$ O2 I) t
) B& ^5 f9 {9 S" Q0 h& o
package com.javacode2018.lesson002.demo18.test1;* t; F9 p7 ^  C; u8 U
  J1 J" \3 H) U# v% J
import org.springframework.beans.factory.annotation.Configurable;% ^' p3 s! [4 l1 u
import org.springframework.context.annotation.ComponentScan;3 D  ]# D/ n3 C
import org.springframework.context.annotation.PropertySource;. I2 j6 O; u' y' E2 T

/ q3 t- ?& M; C6 E@Configurable
2 W# ]& t9 e; V! L% {. g; q8 @. e@ComponentScan" k& F9 b$ k6 a& j5 m& X
@PropertySource({"classpath:com/javacode2018/lesson002/demo18/db.properties"})% B1 j/ c. b8 o' p7 z$ N9 V4 [' l  }: a
public class MainConfig1 {0 V% t" E2 q6 g
}4 H* Y6 c9 i7 S" W" }# \: o
来个类,使用@Value来使用配置文件中的信息
5 u3 U0 j& M8 e- Z
1 w1 d4 Y* V: z3 ?  Ipackage com.javacode2018.lesson002.demo18.test1;
! g4 F" {% U& k$ F
2 }2 `7 c3 w2 G+ \import org.springframework.beans.factory.annotation.Value;/ h9 t$ A+ J6 q4 w( [" w
import org.springframework.stereotype.Component;3 J9 }( Q% z' ]7 |2 L

; g; Q8 y% P! t1 M% E- F6 {" w@Component- W: L* H  r- q+ m1 R* D5 v& h2 f
public class DbConfig {2 s, k/ D1 N$ N/ L
2 w% n: }; e, @
    @Value("${jdbc.url}")% ]+ k% }7 X2 n/ J* z
    private String url;
0 R5 i) |8 p6 b/ S6 X. G* p" L5 G) L
    @Value("${jdbc.username}")
* ?3 a3 F$ a' e5 C% r! D9 m    private String username;
% H5 ~" N5 i2 ?1 W! c8 {
0 d4 P9 M) A! x0 E. R* u    @Value("${jdbc.password}")6 u0 C; u* g+ L7 N+ n1 c, V# L
    private String password;
+ h+ L: d0 S1 S' p# m; D/ U& r& `9 N8 s2 [, Y
    public String getUrl() {
, ?! v- W, X' [5 Y1 j8 x        return url;
1 B1 J* m- _# T. B4 h5 n    }  Q9 C& K% z: E2 @7 p; t

: V2 ?4 Y, i2 K* j    public void setUrl(String url) {
% u  O- s7 j: ~( D3 _8 y        this.url = url;8 V" _% |2 N& |& e& z
    }. z  u% ?7 @+ }1 _

! D% \. t3 t1 X4 U; v+ c    public String getUsername() {8 K1 S" `, ~: ?
        return username;" k# h: P; J) P. L/ A$ i3 v
    }) N+ v: s9 u/ n2 w
5 R! n, F: [5 ]. ?9 e
    public void setUsername(String username) {& @# s( |; {* s  w( ~4 U
        this.username = username;
& y+ w1 L% f! G4 E    }& K8 f9 a, |2 N+ P  i7 D

9 C4 b5 m" q$ {0 q7 ~    public String getPassword() {, v- F+ ^8 o, \4 c
        return password;: z7 H9 e/ _  {
    }1 ^( b: K8 q3 U- @
! E+ T# K' H+ r; M1 n% h0 p9 _
    public void setPassword(String password) {# f& C8 T: a2 y
        this.password = password;
0 Z* K1 y: R* [0 g    }: Z$ b, z. X; }& D2 ]
8 K9 p  L6 O) N0 U. g3 {
    @Override
% H. h$ r% d- c. Z    public String toString() {
4 k( x  s, \! u6 g        return "DbConfig{" +
3 q* ?0 D2 T" e% k1 D                "url='" + url + '\'' +
5 J) i5 o5 M' l# {+ h% A+ o1 @& S                ", username='" + username + '\'' +
. q; b- ^* }' V5 d% B8 w3 `                ", password='" + password + '\'' +
6 d) {8 w: K, \( H& E* ]                '}';9 B7 W8 P* n9 r$ ^
    }! g0 U6 {  X1 c8 q& p
}
3 f) }: Z+ z+ U5 K9 |2 M: S% j+ r上面重点在于注解@Value注解,注意@Value注解中的3 W3 V/ ^7 N3 Q8 n4 ~
1 ?- S6 H5 v9 H9 Q
来个测试用例  m' y4 x5 D' A, A4 P
$ `5 z1 t( c4 c1 x7 N$ N* w
package com.javacode2018.lesson002.demo18;; p* C/ F5 f6 W7 z

. W1 z/ k: r2 C4 bimport com.javacode2018.lesson002.demo18.test1.DbConfig;
$ L" r4 {* N- n$ v$ l) ?' F# \& aimport com.javacode2018.lesson002.demo18.test1.MainConfig1;& u  S+ k0 m# C3 T: [
import org.junit.Test;! g' |9 F+ A# b0 g% R' P
import org.springframework.context.annotation.AnnotationConfigApplicationContext;3 L- a+ m+ i8 U. ]8 y
' c6 p9 s8 u& s8 z) A% Z
public class ValueTest {
: o1 k  G4 l/ N% W  t- j
+ [/ a3 ~: g3 i% S( ~    @Test  f8 A) ]* \5 V6 k2 E
    public void test1() {
. [( i4 K5 h- [3 F5 e+ Z! _7 @        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
' Z4 c, L/ S( O% }  X        context.register(MainConfig1.class);
7 q2 [# m1 t& s        context.refresh();
7 W" @! w& Z9 K7 e. Z) K$ |' s5 H: A( }3 Y. m3 }
        DbConfig dbConfig = context.getBean(DbConfig.class);
0 M7 C8 a7 E0 [, ~* j  r        System.out.println(dbConfig);
  M$ O( q- ?0 U* V    }( \& h' C, q6 m) S7 I# p
}
2 q  i' T4 V/ g运行输出
2 e/ H" Q' Y; U0 v2 W) ~( l; S& l1 L" t9 G# ^2 @1 y7 w
DbConfig{url='jdbc:mysql://localhost:3306/javacode2018?characterEncoding=UTF-8', username='javacode', password='javacode'}
# E$ ?: X! j' }7 O1 r, i上面用起来比较简单,很多用过的人看一眼就懂了,这也是第一个问题,多数人都是ok的,下面来看@Value中数据来源除了配置文件的方式,是否还有其他方式。4 A* b4 U1 Y& O, U2 [9 X7 E

" r% E& g. \8 G4 ?@Value数据来源
- ]0 t. A" f/ l# x  N/ z" d
5 Q7 p" P. g/ v通常情况下我们@Value的数据来源于配置文件,不过,还可以用其他方式,比如我们可以将配置文件的内容放在数据库,这样修改起来更容易一些。
; v! ]* p" g3 c3 a0 b4 _- R) K' t) W: @0 J
我们需要先了解一下@Value中数据来源于spring的什么地方。
9 U; x3 r' d! b0 J+ J& ]. x& N$ Q0 L( g$ X% b2 T# M9 G7 S
spring中有个类! {+ Q* Y& ?7 K- l$ t( |6 y4 a/ M

9 d' _9 j5 _; dorg.springframework.core.env.PropertySource# G  Z* h6 g+ h
可以将其理解为一个配置源,里面包含了key->value的配置信息,可以通过这个类中提供的方法获取key对应的value信息
( ?' L$ Z0 c8 p% D" |% s) G8 ~9 `1 m, o! f2 r7 E$ n
内部有个方法:
' ~5 V3 T/ x# ?- h* ]1 _! S/ I: E, m  l$ p
public abstract Object getProperty(String name);
: Z/ V* _  z# M3 l3 G% J# m8 _通过name获取对应的配置信息。
( ~6 v. }5 B7 Y0 z. v, U. A! R% h7 {* L- X6 |5 E7 j
系统有个比较重要的接口% N1 f, e8 h* y4 V
- ]/ {7 `- o0 U- U( v9 [" Y1 X
org.springframework.core.env.Environment7 \$ i6 h8 j: b- ?( X
用来表示环境配置信息,这个接口有几个方法比较重要% F8 J8 s; V! m* I" K  S

+ [. \0 k! R1 D4 R4 TString resolvePlaceholders(String text);' W& l, S- F& V( ]8 d; G
MutablePropertySources getPropertySources();2 U8 m1 Y. y2 _; k1 T/ L) O5 i
resolvePlaceholders用来解析${text}的,@Value注解最后就是调用这个方法来解析的。
5 k2 Z, e8 j7 @+ \0 ]  V5 y1 A
4 D  e9 U. g* N) r& Q7 dgetPropertySources返回MutablePropertySources对象,来看一下这个类% U5 ]% |) ]6 p! G

: X( _' t' D8 Opublic class MutablePropertySources implements PropertySources {' l  x- k6 j8 t+ y) u( F

7 M" F% R) i5 p3 [    private final List<PropertySource<?>> propertySourceList = new CopyOnWriteArrayList<>();% o& h* O$ v9 M$ p8 W- f1 m) N
7 p% v' l5 D+ |/ d
}
  @5 _8 P9 D( b4 W$ N1 p# h内部包含一个propertySourceList列表。; \. ?( k* E, y9 @5 N0 |: x

9 L/ M: z0 z; r/ ]) S' |) rspring容器中会有一个Environment对象,最后会调用这个对象的resolvePlaceholders方法解析@Value。; G9 ]9 v( s& u

$ X" {4 `% Y" q大家可以捋一下,最终解析@Value的过程:1 g2 b! x, E* X

$ L9 a; ]; E. E' k" a1. 将@Value注解的value参数值作为Environment.resolvePlaceholders方法参数进行解析
' O9 d6 r2 f# o% Q5 l4 N  e/ D2. Environment内部会访问MutablePropertySources来解析3 T' t: f' y; ?  h# B: |6 z
3. MutablePropertySources内部有多个PropertySource,此时会遍历PropertySource列表,调用PropertySource.getProperty方法来解析key对应的值3 P! A9 ~" V6 E5 l) ?$ H$ s
通过上面过程,如果我们想改变@Value数据的来源,只需要将配置信息包装为PropertySource对象,丢到Environment中的MutablePropertySources内部就可以了。) P8 K0 G4 c0 a' d3 A
1 |8 o4 z9 w  ~( T: b4 E2 G
下面我们就按照这个思路来一个。
# a# J/ F5 ]- E! U( O
' `  f/ U/ S: b- Q4 [来个邮件配置信息类,内部使用@Value注入邮件配置信息
8 v+ B0 v+ ?1 m* T( q, F! A5 U% U0 S0 v) y( H( @
package com.javacode2018.lesson002.demo18.test2;5 P; [8 }9 P& }" a
$ _& ~+ k+ A; k$ y5 V
import org.springframework.beans.factory.annotation.Value;
3 m1 ?9 C2 p* g2 n% ximport org.springframework.stereotype.Component;7 M' h- ^' ?5 U" |3 K' J9 C

9 ^' `. n* R* r0 e/**) T) c8 x$ o! Z- i/ c3 n0 P; C$ _9 h
* 邮件配置信息9 {4 B: |0 o& Y
*/4 `" H0 E: T) Y/ O
@Component
! q# y* d) C) ^  _, rpublic class MailConfig {/ @) J1 N6 `3 X. a$ |

) U2 ?8 R0 @& P7 N    @Value("${mail.host}")
/ x7 Y  e$ b# V3 ~$ L5 m6 E" e    private String host;
, n) W; {9 b0 |' X2 @6 S& W
1 C6 z) v0 i/ P, q    @Value("${mail.username}")
8 F7 D% k8 s/ ~5 V    private String username;8 v4 _$ ?: D, E) |. F1 \% N: \
0 o* S& r- r' N
    @Value("${mail.password}")4 @) f2 C  J+ k# o2 C# \
    private String password;
- I7 N( |5 k9 i6 j
7 J1 S6 B4 S2 V- q! ?" u    public String getHost() {
* ~. u! R8 B3 }- Q. Z        return host;
3 o. b  F4 y+ [) W    }& i, Q: p& I) o3 k
+ Y' w- e; [, V# {% g
    public void setHost(String host) {2 o, C( U2 r: g
        this.host = host;" d/ Q1 s0 ]7 P: }' J# v- z" h
    }) b+ A7 b. g& f
, Y2 [" k7 J* l/ P, S( w
    public String getUsername() {
; o9 c3 q: g+ P9 V; W) F1 U        return username;
1 W  i7 J; M. c  }8 F) m: j, n    }* n6 M! v- ?5 Q! Z

8 [8 y- _7 c" _, s    public void setUsername(String username) {
$ U! ^" V- U! x+ k/ N) W) E7 c        this.username = username;
4 o" A. S6 ]+ |6 Q    }
, [8 S( g' p0 c
3 y9 L$ W; r, d, E    public String getPassword() {5 T/ ^+ u4 [: ?$ {5 l
        return password;; |- r' A+ {9 [0 j
    }/ Y2 J2 _7 h2 s. Y0 i: M$ y2 s/ h

! i% V; V2 C! o    public void setPassword(String password) {5 b+ @. q& g( m! W3 C
        this.password = password;8 c5 r; u. `* R' ?
    }
) t0 o0 V8 h6 n+ K3 O% W2 y3 a. [! \" r; B
    @Override2 Z  r7 D7 G4 N, ^5 E' s/ r: G
    public String toString() {
0 I; U: e/ Z  k; y$ @        return "MailConfig{" +
& Q0 U) H  p# m8 S: H                "host='" + host + '\'' ++ E. n' _# W5 M
                ", username='" + username + '\'' +
7 w: t# s4 _) q6 t: h                ", password='" + password + '\'' +
7 |; h4 g, v6 t7 P                '}';
: x5 y6 U  |# S2 k, {# [    }
/ V) p3 K( X4 j' R: P}
& _) v5 ~' _6 L; I再来个类DbUtil,getMailInfoFromDb方法模拟从db中获取邮件配置信息,存放在map中) E3 H- H. p$ D- n* i! `4 B
# K: o( m; _; g0 j
package com.javacode2018.lesson002.demo18.test2;8 [" j; V, J* @6 R  {

" a9 U: G4 f9 T. I/ ~import java.util.HashMap;
- G' Z' A7 {0 T" [import java.util.Map;) K1 [4 ]. G1 s( u# B
/ {; f7 e" o# Q, \. D+ C2 I8 j
public class DbUtil {# e( N  g6 s  M- `) }, J3 c3 ~
    /**3 w" i4 m' u. o( J1 _( Z
     * 模拟从db中获取邮件配置信息
$ |, ?- J8 E7 ^2 X( p, @: }     *
2 n5 O9 W( E) A  h' Z  S     * @return% k% M2 X( W: D& l# ~/ t
     */
% [* a3 H, }, `, a8 y    public static Map<String, Object> getMailInfoFromDb() {
' q+ |+ l" I. t' a; r4 K2 P% e        Map<String, Object> result = new HashMap<>();
% V/ b/ B$ r: L: b) C. s8 E        result.put("mail.host", "smtp.qq.com");9 v5 l: k6 s1 s' P
        result.put("mail.username", "路人");. ~% d4 H4 E' J
        result.put("mail.password", "123");
% ^* E3 j/ s4 K# H) Y        return result;7 X) l1 {% ^$ r5 Q
    }' \/ c5 [' H# J9 M0 ^
}5 z1 ]" m, L; S0 o0 x# h
来个spring配置类
3 w9 A& a- I: t4 W1 S
1 n9 T7 c" c/ ^& vpackage com.javacode2018.lesson002.demo18.test2;
4 _- z0 B! P/ ?' `& s2 u5 F) g0 S% j* v
8 [# v% q! Y% l$ h+ cimport org.springframework.context.annotation.ComponentScan;
3 x/ j1 x  t! v: H8 o4 ]import org.springframework.context.annotation.Configuration;* T3 g& O' N: M" x' P! }# I6 O
( g8 ?" v8 h6 D* }' s( {! Y; d
@Configuration
$ {" d: k9 |" G3 f/ ~5 c% m* k@ComponentScan8 B/ k) [/ D! P; k) G8 F
public class MainConfig2 {5 b* m- S# e: a, ^# c# u4 h
}  m/ z% u0 \) {; B* H
下面是重点代码
0 S$ U7 k9 p( I' U
. A, N9 ^7 e9 y9 f$ A% |@Test
& I4 m$ Y1 _$ y* Zpublic void test2() {7 C, K( Y# D' }( l) _9 F! [
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
6 L# G1 I$ Z0 g+ j2 S  c: J* t: Q( B% s2 x  a
    /*下面这段是关键 start*/$ n* n; d) \- V
    //模拟从db中获取配置信息
9 @7 E8 Z: K- P2 D    Map<String, Object> mailInfoFromDb = DbUtil.getMailInfoFromDb();
9 F; a% V8 L/ r6 T) X: S    //将其丢在MapPropertySource中(MapPropertySource类是spring提供的一个类,是PropertySource的子类)" }6 d- x' }! Z+ a: {
    MapPropertySource mailPropertySource = new MapPropertySource("mail", mailInfoFromDb);/ @$ k6 a2 t) A8 m- A3 E( G
    //将mailPropertySource丢在Environment中的PropertySource列表的第一个中,让优先级最高) J' x, N7 h' r9 M8 E! q
    context.getEnvironment().getPropertySources().addFirst(mailPropertySource);
9 e5 z3 ?% g5 J" \    /*上面这段是关键 end*/0 O' _2 H& N1 k* @6 f/ U

, ~7 V% {: v7 c4 k4 F    context.register(MainConfig2.class);  X6 @& O/ m& Z' j
    context.refresh();
' T. D% L" s/ @; U8 g    MailConfig mailConfig = context.getBean(MailConfig.class);
+ ^) _6 p* O3 Y8 V* ~' v3 q) W    System.out.println(mailConfig);) y0 z" X5 w2 s: e/ W* {
}
$ H" z0 r: T/ Q- j/ l注释比较详细,就不详细解释了。" O9 ]. }) @9 l% j6 }/ A  @' n
3 m/ q* E% t4 x. Y
直接运行,看效果
) T- z0 e/ Q2 C' w- A, x. d4 y6 Y. x( Y* h0 y# _* ]7 v
MailConfig{host='smtp.qq.com', username='路人', password='123'}
' f+ ^" c! U' o8 h; r有没有感觉很爽,此时你们可以随意修改DbUtil.getMailInfoFromDb,具体数据是从db中来,来时从redis或者其他介质中来,任由大家发挥。
' l, Z8 k7 ?5 [: h/ g& [/ h: d8 U% e; V# `
' ^; J& \8 |; J3 v( ]3 s$ E上面重点是下面这段代码,大家需要理解
) u7 C+ D% m  t4 \( ~) H$ `6 Q9 e7 ^/ g; ^! f
/*下面这段是关键 start*/" s% O; z1 l/ U. O; [
//模拟从db中获取配置信息
8 m% P% q- |' N0 ?" BMap<String, Object> mailInfoFromDb = DbUtil.getMailInfoFromDb();
$ X) w" H9 t6 z3 Y- |//将其丢在MapPropertySource中(MapPropertySource类是spring提供的一个类,是PropertySource的子类)4 O! s! {6 v9 R+ s" c# N
MapPropertySource mailPropertySource = new MapPropertySource("mail", mailInfoFromDb);
6 k$ ^* ^7 O- K/ N6 F- [! r//将mailPropertySource丢在Environment中的PropertySource列表的第一个中,让优先级最高
1 d* ?3 C) t( q- z! Ccontext.getEnvironment().getPropertySources().addFirst(mailPropertySource);
+ Q* F, s  C0 A4 N' I% y! i: s/*上面这段是关键 end*/
$ d! K9 {1 }8 g" J; t3 x咱们继续看下一个问题* _2 m) t/ V# r

6 `: k# M3 w5 O' g6 @- k8 q如果我们将配置信息放在db中,可能我们会通过一个界面来修改这些配置信息,然后保存之后,希望系统在不重启的情况下,让这些值在spring容器中立即生效。
$ t' m: ~" c. {8 w8 i$ \+ g  ~% |) [8 N' y! }5 N
@Value动态刷新的问题的问题,springboot中使用@RefreshScope实现了。, [0 f9 T# B8 b! C. K8 s
  q) x/ D6 j6 J5 O$ b* k
实现@Value动态刷新
: D9 [! {* K$ M6 Z5 U" u
! o# B+ q8 T+ b先了解一个知识点5 f! l' R5 h! y3 ]1 Z; }

4 U! y4 \' o- M' [这块需要先讲一个知识点,用到的不是太多,所以很多人估计不太了解,但是非常重要的一个点,我们来看一下。! z) l! \1 ?% N2 v5 o( G) G; ^
+ k- U" x) I' f% t0 c
这个知识点是自定义bean作用域,对这块不了解的先看一下这篇文章:bean作用域详解9 l% d' J  c$ L. ^$ V3 g
0 H5 L1 P6 j/ U* P
bean作用域中有个地方没有讲,来看一下@Scope这个注解的源码,有个参数是:3 {# Y4 x) h- p& X

8 I# w/ Y) y( H( u2 YScopedProxyMode proxyMode() default ScopedProxyMode.DEFAULT;
7 u7 n: c* e; `2 d3 e7 b, y这个参数的值是个ScopedProxyMode类型的枚举,值有下面4中
, }3 l. n  V% C4 h$ c$ K: l+ A& d* B' z! A7 g8 ^& {$ v
public enum ScopedProxyMode {/ w6 {+ }* [' _( n
    DEFAULT,
  r9 p4 P& ]& E  H8 K, Y9 o    NO,
; U4 n: \2 d! _    INTERFACES,7 `/ j7 c% @! g6 e2 x! g) r
    TARGET_CLASS;+ a3 I3 \- a8 i* I% ?7 m, h# a
}, [8 e, S* F2 ?; b* q$ \4 U
前面3个,不讲了,直接讲最后一个值是干什么的。
! d9 E# b$ E  A: H. M2 j5 d/ K+ N1 w/ A0 H, n
当@Scope中proxyMode为TARGET_CLASS的时候,会给当前创建的bean通过cglib生成一个代理对象,通过这个代理对象来访问目标bean对象。  s) P0 d9 A# |) r4 @

1 d; Q- ]- Y8 s/ D! Z" L理解起来比较晦涩,还是来看代码吧,容易理解一些,来个自定义的Scope案例。. o. b4 X6 A% M' B8 T
" r2 U/ a  Q/ ]3 R
自定义一个bean作用域的注解: Z+ U; e0 K& E3 q

7 ^1 C1 i  q9 t2 ^1 r/ Hpackage com.javacode2018.lesson002.demo18.test3;6 J0 U" a$ a+ {2 U% O

4 [+ J9 E! i6 r& @' l- Timport org.springframework.context.annotation.Scope;  r% @( i( C: P& w# ~
import org.springframework.context.annotation.ScopedProxyMode;, Q+ ]' s! W( T& M$ y3 ?

! L& r) U2 q' J% Kimport java.lang.annotation.*;8 @3 ?: J* z9 G! B, D( N# j/ J
1 V, Q" y; b2 |0 C2 h
@Target({ElementType.TYPE, ElementType.METHOD})
2 H& z$ q6 V# o+ o@Retention(RetentionPolicy.RUNTIME)
5 f) W: K6 M8 y. V@Documented; I+ y. J- e( x% l( l0 q4 u
@Scope(BeanMyScope.SCOPE_MY) //@1* ~# e) V  [- U
public @interface MyScope {
" T' a9 X1 K; I) g    /**1 B# N9 O8 B$ y( i+ J' w8 F
     * @see Scope#proxyMode()3 @# M" G1 G8 v5 m' h
     */
4 g* V) F7 [1 ?/ B1 B: f% z+ W, O    ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;//@2- Y. V3 g* Z$ ^1 x! r5 R8 R; b
}6 d4 b1 J) ?5 K3 Q
@1:使用了@Scope注解,value为引用了一个常量,值为my,一会下面可以看到。
* ]! i  A% o# n# Q+ f! [# _! Z9 h+ v7 }7 ?. G1 u8 }0 O
@2:注意这个地方,参数名称也是proxyMode,类型也是ScopedProxyMode,而@Scope注解中有个和这个同样类型的参数,spring容器解析的时候,会将这个参数的值赋给@MyScope注解上面的@Scope注解的proxyMode参数,所以此处我们设置proxyMode值,最后的效果就是直接改变了@Scope中proxyMode参数的值。此处默认值取的是ScopedProxyMode.TARGET_CLASS
" G( i7 B& ], h+ ], w/ ?
: M) G/ i5 n9 t6 ]@MyScope注解对应的Scope实现如下, n) i  p6 K! W# d" \
) J+ v* @# ^+ S9 N4 c. m. L* e
package com.javacode2018.lesson002.demo18.test3;1 v: v4 W" X# I* l- Z/ q5 Y: _$ G
0 M2 U, ~3 a0 p) ]" f3 f" H1 w
import org.springframework.beans.factory.ObjectFactory;
. `; t9 |% z* I! Vimport org.springframework.beans.factory.config.Scope;
6 n& o" k+ I5 j* Mimport org.springframework.lang.Nullable;# r% m7 K0 J# M

- |/ {; B3 |% t. Z/**# n1 `8 M2 @* v! _" t
* @see MyScope 作用域的实现" S6 u# N3 A  h" O* l& z  k
*/! J/ ]2 a' P5 h0 I4 N$ K
public class BeanMyScope implements Scope {& v/ X/ M, T+ Z5 A

. y8 h, i3 o; J# I4 U    public static final String SCOPE_MY = "my"; //@1/ }; f, a6 P; }7 i

. s/ p# P! ^6 d    @Override
5 g; d0 e+ I6 ?* M% I2 i    public Object get(String name, ObjectFactory<?> objectFactory) {
! ?2 `) w* |$ d2 j  G, B        System.out.println("BeanMyScope >>>>>>>>> get:" + name); //@2
! _' N3 X$ W4 V2 b* j( A        return objectFactory.getObject(); //@3
: x" K" y: u" G: L8 L3 Q/ i    }
/ R. ~# W, H% ]( s5 n* X/ `# d( X5 @6 Q& h2 i& G
    @Nullable
5 N) B0 g& h7 g/ w. j    @Override. a* A# ~  K% M0 v* p9 ~- c
    public Object remove(String name) {
* m2 o0 a9 }, n$ j        return null;! f7 J) E' B# B. ]# Y2 C
    }) m! B2 ~% N4 @
* n% O, k+ Y8 }' U, S8 Q, E. `
    @Override
3 \" Y0 X) R9 p, S8 \    public void registerDestructionCallback(String name, Runnable callback) {+ a) M. R+ x& H* K
9 Z' M6 d7 E" [, h+ V6 i  \
    }
6 G" K9 y. u$ J' M# i
( ?2 H8 S9 z" f) j0 n    @Nullable& A# A5 X! c9 B
    @Override! c- t( L& ~" S; s6 `' M: e$ t
    public Object resolveContextualObject(String key) {
* d* W/ q! A) o# D2 K/ w1 J        return null;( |2 |* P8 ?; s" n- b" _
    }" Y# E+ m& f- Y% \

3 `# n- w6 H- w; b8 r8 `    @Nullable
( d/ W$ F- f5 J# B" d0 ?    @Override6 Y9 E8 D9 g/ @2 Z
    public String getConversationId() {
" j) Z$ A5 x% M        return null;0 A5 v! H. y; ^
    }
& u2 j$ K. S* S) f& t}
" o7 i" S- J- v3 \7 I# o@1:定义了一个常量,作为作用域的值- `- z1 F4 h- U5 x" d

" w: Z$ ~" o) u" v! k@2:这个get方法是关键,自定义作用域会自动调用这个get方法来创建bean对象,这个地方输出了一行日志,为了一会方便看效果- U( G! ]0 z& s% r  ]

0 t, J5 S, N% t# [$ x1 u- D) E@3:通过objectFactory.getObject()获取bean实例返回。* W* K: h! ~/ |

$ N/ y6 }2 U; R; q( K: Z1 m下面来创建个类,作用域为上面自定义的作用域/ {, R: x& D' G; I3 |0 ]8 v/ k. D

5 D2 R* }$ I$ Y7 r' a& _package com.javacode2018.lesson002.demo18.test3;
: K( V# m( h' t- @8 T) j
4 I$ a# y2 e! [5 ]1 w# gimport org.springframework.stereotype.Component;
) t, ?: C1 o% `; z+ z+ }) a2 h  B, N5 y% e' {
import java.util.UUID;
! x# G) I9 n% a/ M
- _: h; u6 F. S. x' J@Component4 K* K$ q3 Y& H
@MyScope //@1 : e# I6 Y" L* m: N
public class User {
1 {! ^" D2 \( T) h4 ?5 G( O% y, R2 D
/ n  H) j# ]. A    private String username;1 |; t/ p* t- ~" q

4 A. x  T7 k( I+ v. Z0 U* o2 W( M    public User() { 1 `+ ~+ z3 G% h1 v- W4 u
        System.out.println("---------创建User对象" + this); //@2: o1 g. b2 g$ }
        this.username = UUID.randomUUID().toString(); //@3  a# G  p7 C$ n
    }! O& U7 ~( ~2 N
1 v8 a# T! Z6 H+ l3 o/ P  q! {
    public String getUsername() {
+ d" i* W) t6 x        return username;
/ R" m2 V$ {8 X. z/ |+ i    }/ h+ ]* z- _; w: y4 A

! i+ O; z2 {6 d* _& Y* s  C    public void setUsername(String username) {
# p( U; F9 `$ J) ]/ J6 ~9 v) q+ s        this.username = username;0 A+ E' n0 r$ Z7 h9 v3 c
    }
6 o- c* W$ k0 [+ \8 P
/ c4 B5 ?7 B  {$ V}
! F5 k+ r) }7 K: t@1:使用了自定义的作用域@MyScope& O. A( B/ L5 U( q9 ^  _

  s9 l5 c' Y2 q4 R: l: g@2:构造函数中输出一行日志
5 s5 F, B( H1 Z: o" ]6 e; x9 m* J  J# g( R# o: l
@3:给username赋值,通过uuid随机生成了一个
9 _( y: S1 u4 d$ D# M8 e! {
# D9 r- f& x+ `, Y来个spring配置类,加载上面@Compontent标注的组件" O& d3 K6 J0 e7 z/ Z& D) x: r

4 |' p- f& L& W4 f( M$ O2 upackage com.javacode2018.lesson002.demo18.test3;+ C" u4 {- W+ M% ~" j
) N4 e* W1 z+ S, N' I7 w9 |
import org.springframework.context.annotation.ComponentScan;
# V4 \) _: l# Q8 ~, `import org.springframework.context.annotation.Configuration;& s5 z$ s+ N& O1 Z. {, e

. v( q* R# v5 V3 e( Y@ComponentScan
( [$ Y+ n$ Y1 m0 J0 W: k* z* X@Configuration
8 y; i( ]: U0 x" j0 q; Qpublic class MainConfig3 {
- u1 l6 Y! d+ M1 R* N9 Z}4 x" a& Q) r2 L- E1 e
下面重点来了,测试用例: K( v. q# T; Y" y2 B
' M% C& }, i: O6 z: a1 W
@Test
! \8 X2 i1 E) }+ q  ~, bpublic void test3() throws InterruptedException {
% X1 U7 S+ w. I4 E- z. P! u    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
7 v2 z' d0 l+ _, d+ M0 C& K    //将自定义作用域注册到spring容器中
' F, U! c8 {0 a! e2 j    context.getBeanFactory().registerScope(BeanMyScope.SCOPE_MY, new BeanMyScope());//@1
* R+ w" U  f3 @5 I: F    context.register(MainConfig3.class);
& g; w" M" W2 I5 j/ p' q7 S- u    context.refresh();
5 J: z& r2 u1 B7 T  c; s+ h4 n( T+ I7 J6 I6 ^, O+ v* d
    System.out.println("从容器中获取User对象");
0 g8 x" |* Y; O' X" O4 l7 p    User user = context.getBean(User.class); //@25 ^8 Z  I: j4 U6 L
    System.out.println("user对象的class为:" + user.getClass()); //@31 j  R2 X2 Y+ e' {) i7 O/ L, `
2 z6 |0 i1 r4 O, F
    System.out.println("多次调用user的getUsername感受一下效果\n");
. P- U% ]' O6 h    for (int i = 1; i <= 3; i++) {$ |  `& Y3 m) o
        System.out.println(String.format("********\n第%d次开始调用getUsername", i));
* d: o$ U2 H4 M/ w- c' L        System.out.println(user.getUsername());
. w1 r" M$ t  ?* x. r        System.out.println(String.format("第%d次调用getUsername结束\n********\n", i));
1 H  ^0 p& q7 p* Q  L. o    }/ s4 @7 i5 h$ i- A. Q6 G
}# I+ y6 ~. j, \. k6 a0 [6 I
@1:将自定义作用域注册到spring容器中& p- c! V% l$ \" t' g* o. x2 w

3 ^" w( k: @! ?& Z% _@2:从容器中获取User对应的bean$ c0 Z& y" O7 T1 b0 y0 @+ m

2 {; c! L) J3 b@3:输出这个bean对应的class,一会认真看一下,这个类型是不是User类型的& E* U3 W0 g& E9 _1 {

3 g4 q" H6 |' V5 x代码后面又搞了3次循环,调用user的getUsername方法,并且方法前后分别输出了一行日志。, C# M1 ~8 W- l4 L8 d

7 u( _7 C  H# F: s- D  I见证奇迹的时候到了,运行输出
: R$ q/ D" |' P. Z, ]
$ ^( f" s  D% L5 {从容器中获取User对象
" G$ P, e5 ]  r: ~% G8 `user对象的class为:class com.javacode2018.lesson002.demo18.test3.User$$EnhancerBySpringCGLIB$$80233127) U& h' u' I4 f8 f6 {( U* t5 a3 [
多次调用user的getUsername感受一下效果
6 q$ O. ]4 ^: T/ F% Q% L- N5 r4 Z  J& ]
********: {, ^  ?, l; t! U3 Y/ }$ m8 a' E; W
第1次开始调用getUsername
, B3 {! m& X# r" P  S( u3 ^BeanMyScope >>>>>>>>> get:scopedTarget.user
1 w8 }8 L  b# ]---------创建User对象com.javacode2018.lesson002.demo18.test3.User@6a370f4
: w/ r' u6 f8 R- l+ P7b41aa80-7569-4072-9d40-ec9bfb92f438+ ], e8 x5 I" |& D4 Y1 P- J6 y
第1次调用getUsername结束! w+ K' Z& c. r5 ?) z% I" P9 |5 T
********! w+ G" `$ r$ Q; ?; _: ]5 Y% O

! f7 k% x1 ~2 g( v9 U********
4 n- T! t/ ]' y第2次开始调用getUsername
( x- z& H+ M2 Y4 YBeanMyScope >>>>>>>>> get:scopedTarget.user
* Z% ^6 P% j1 n---------创建User对象com.javacode2018.lesson002.demo18.test3.User@1613674b- o" }4 e1 c3 t
01d67154-95f6-44bb-93ab-05a34abdf51f
: G% }( ~! I/ ^, p) k第2次调用getUsername结束1 L9 `# D% v( b4 G
********2 m! v& b4 @3 `" m

$ k$ Q1 V  \9 L7 n3 ]2 x% w8 I********
" B9 b% \7 I( a' l% Z1 P, b4 M( x0 |第3次开始调用getUsername
$ ^4 ~  O5 O6 I9 ^& |3 HBeanMyScope >>>>>>>>> get:scopedTarget.user5 b. L) r- X+ i- V# I& z; j
---------创建User对象com.javacode2018.lesson002.demo18.test3.User@27ff5d15
+ s! n0 D/ z5 B7 x3 O8 |4 \7 w( S; O76d0e86f-8331-4303-aac7-4acce0b258b8
) E; k# U+ F) E+ |/ w5 }( W2 n) t' h第3次调用getUsername结束( Y  V& Z8 I. \7 y8 V7 k
********. @9 l7 j9 ~) X$ Q1 \$ K1 {! N
从输出的前2行可以看出:$ K+ Y! ]4 O+ v. _* y' W; I
  |; w7 W* H! q
调用context.getBean(User.class)从容器中获取bean的时候,此时并没有调用User的构造函数去创建User对象
) n1 x9 t0 s5 S1 {3 h+ z6 }# |
6 {, Q. q7 q1 k0 \+ A' W4 R/ z# c第二行输出的类型可以看出,getBean返回的user对象是一个cglib代理对象。
0 @/ k7 E# e4 T5 Q2 {7 o/ T! ]9 L1 J$ _. V9 }9 R/ h. _# ]
后面的日志输出可以看出,每次调用user.getUsername方法的时候,内部自动调用了BeanMyScope#get 方法和 User的构造函数。
  {  @1 C4 f# e7 h' z2 l7 Z6 L  E8 P7 q, Y
通过上面的案例可以看出,当自定义的Scope中proxyMode=ScopedProxyMode.TARGET_CLASS的时候,会给这个bean创建一个代理对象,调用代理对象的任何方法,都会调用这个自定义的作用域实现类(上面的BeanMyScope)中get方法来重新来获取这个bean对象。0 @1 [8 t) h! H" @

  P* r3 B* _  Y& i2 e' a3 H& R动态刷新@Value具体实现
3 k4 r- M% {& B' @' W+ y
" }8 t5 C  W5 K2 }, W那么我们可以利用上面讲解的这种特性来实现@Value的动态刷新,可以实现一个自定义的Scope,这个自定义的Scope支持@Value注解自动刷新,需要使用@Value注解自动刷新的类上面可以标注这个自定义的注解,当配置修改的时候,调用这些bean的任意方法的时候,就让spring重启初始化一下这个bean,这个思路就可以实现了,下面我们来写代码。6 u# c5 d) a0 W5 \  v0 X
, [# R3 O! T% l% E
先来自定义一个Scope:RefreshScope1 S! B( P/ s3 O5 p6 `3 O
, c7 {/ Z2 u1 K# M; Q! f
package com.javacode2018.lesson002.demo18.test4;
& ?( s. ]& S( U1 E+ c. S
8 Z9 }" r" m3 o; ^6 ^5 limport org.springframework.context.annotation.Scope;! D( F5 p$ E6 |$ L4 g
import org.springframework.context.annotation.ScopedProxyMode;0 I% f5 l9 G0 }  j4 o& P
' m; S" F9 J! C0 [9 G
import java.lang.annotation.*;" G' Z# E9 X7 ^5 }- ?

  ~. K" \: ]. U% [- d@Target({ElementType.TYPE, ElementType.METHOD})
' |2 x: ?6 S" @) {@Retention(RetentionPolicy.RUNTIME)
  V- s1 C1 G+ B9 y/ C' ~+ ^9 `; d@Scope(BeanRefreshScope.SCOPE_REFRESH)8 c; h( G- [  C6 }7 D& i0 {
@Documented
1 {0 O$ @! y* ^# npublic @interface RefreshScope {
# y. k; j' v9 i    ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS; //@1
- E2 }' S" f1 `% j/ w}
( k) \% G# f8 P' ]: w/ i7 q" s要求标注@RefreshScope注解的类支持动态刷新@Value的配置
, t; H/ T% m: k9 Q, H4 m8 _, A
@1:这个地方是个关键,使用的是ScopedProxyMode.TARGET_CLASS
- b% r" [) s, C( q/ Z; z' e( d) r
, @- e4 u! V* J3 {这个自定义Scope对应的解析类
( x0 f) N2 T$ u( [9 w) ]" `. ~* m
下面类中有几个无关的方法去掉了,可以忽略7 A! N$ n; a' m9 e! m" Y
# `8 j" e  j8 w6 O
package com.javacode2018.lesson002.demo18.test4;
7 [+ ?- s* _( R% w' `
6 M, o/ O. C& a1 B+ h3 M' h. L2 w: m- O
import org.springframework.beans.factory.ObjectFactory;
- g# V6 i8 p0 X, o7 N0 U- m8 Mimport org.springframework.beans.factory.config.Scope;
# N3 u. A- G' mimport org.springframework.lang.Nullable;4 Y9 ~3 i  N  \
/ m  Q, V4 @: M
import java.util.concurrent.ConcurrentHashMap;/ x' S7 f; G; {7 g! j7 e9 h5 t
( a0 d6 F' S# D. ?! S. w3 j
public class BeanRefreshScope implements Scope {7 X! s% T" u7 \

& s$ S) e1 C% Z7 y6 s    public static final String SCOPE_REFRESH = "refresh";
; V6 C  V* t4 m( N
; O& Q. L- k6 ~' F% i    private static final BeanRefreshScope INSTANCE = new BeanRefreshScope();) ]6 q1 ^* z2 B
- M% u2 h/ i+ V' C  X8 i
    //来个map用来缓存bean% x4 v) _( Z" j5 h$ J
    private ConcurrentHashMap<String, Object> beanMap = new ConcurrentHashMap<>(); //@1/ d8 k6 |: O; a, b) t2 N/ W1 [
. d4 O: [! c- P& _
    private BeanRefreshScope() {
" i& y; h+ ^2 @    }$ ^2 y3 l0 b0 z7 P' j1 n8 s

6 Z! s0 i9 ]$ g7 H4 r$ t2 T+ R( _    public static BeanRefreshScope getInstance() {; g! W8 E6 {% o' @; ~) z
        return INSTANCE;
! C( q+ j! m+ q$ f" l0 I    }/ T  K- F9 o' v& Y; q

! t' r, [; m) w    /**! _) U2 V$ X4 ~* ?: u# H' u: B
     * 清理当前
2 E$ S! F, m( B9 x     */
7 `; V% I0 ^2 e6 n" z% K* T4 x: r    public static void clean() {* e1 X, D$ P3 D) P% U
        INSTANCE.beanMap.clear();
- ^1 X1 _: I- w" D/ b  W    }
1 E7 J7 k% k3 A2 W1 R2 O+ K" h9 \7 I+ K+ L/ N0 _
    @Override
. {1 r  D! l6 v" y    public Object get(String name, ObjectFactory<?> objectFactory) {
& A8 j% t5 n# J/ [/ _& d7 [1 W' w        Object bean = beanMap.get(name);
: t, Z$ a+ O$ o! b        if (bean == null) {5 F1 x( L9 K2 a* H" B  {
            bean = objectFactory.getObject();* L+ y( p# T$ s& A3 d
            beanMap.put(name, bean);
/ Y6 {& d, Y& Y% a. G        }
* \+ d* d% q$ M# Q- M5 v        return bean;
3 C& l, J5 J+ T/ F4 @1 P1 v7 H    }, B: p1 B8 b7 H8 {
3 r- v, \0 K, \
}; R5 h$ d; `$ M/ j/ d
上面的get方法会先从beanMap中获取,获取不到会调用objectFactory的getObject让spring创建bean的实例,然后丢到beanMap中
: a" R# a9 p+ M8 _! o+ X4 `
" {; H5 L+ Q: {$ d; t: N1 C上面的clean方法用来清理beanMap中当前已缓存的所有bean
, C3 l1 K% ~( ^" ?4 z* H4 S, V7 K
1 Q4 y% Q+ B+ S, Q9 w) D来个邮件配置类,使用@Value注解注入配置,这个bean作用域为自定义的@RefreshScope( I5 \( n, I, i9 i

' q  {2 x/ C( ]package com.javacode2018.lesson002.demo18.test4;) a- D3 z7 C. A: x
4 E6 R; L5 x/ s; m! Z. M
import org.springframework.beans.factory.annotation.Value;! p4 G3 z& b4 h' W
import org.springframework.stereotype.Component;* O" X8 W* E" S/ b8 U7 j* r

! E3 H4 b6 g" P! i) c+ r/**
* H7 X# i7 ^2 Y) @ * 邮件配置信息
; p- t4 F: _- e9 e! c- y: M */; l4 l2 y3 [# f, q. H2 j6 N
@Component
6 U( A. z6 \7 L1 x0 z! Z@RefreshScope //@1
+ z) ]1 L7 ~" }- }public class MailConfig {
0 X& i) ]* J1 e3 z2 B1 h
4 n4 S* G; M( L& s( F    @Value("${mail.username}") //@2' `' w. l4 l* J) w# }
    private String username;5 k, `: M% n" t1 J2 B

4 M, ?, l7 q; V: p. ~4 Z8 Q4 H7 u    public String getUsername() {
  O9 p6 @# V1 L/ a2 r9 F! V        return username;
% j0 _( U9 [! `# U9 ?' f    }
& g) t: b, L/ F& q; J8 x1 H& k( h5 i- d2 p9 ?8 s3 f
    public void setUsername(String username) {+ N# P+ F2 w; O, |- }
        this.username = username;
9 a6 |8 G+ v7 d    }' a. M5 U. `! c+ ]9 W

& R# x+ ~. c, N  W/ b- J5 b    @Override
/ P  i" S) j! q  j' t- v5 w    public String toString() {7 F8 K; s8 ]6 m* f9 r' \
        return "MailConfig{" +8 z, ^# S1 B/ e
                "username='" + username + '\'' +& K9 K3 n, s+ a) s5 U* D
                '}';' w+ o/ P) I" X% ?1 j/ d
    }
! @. N4 a' Z$ F5 e- l% b; `}
& r8 f; N2 k% w$ y* |@1:使用了自定义的作用域@RefreshScope
7 A4 T3 j1 o- S; p3 o3 g% y& @* {) j2 D9 s
@2:通过@Value注入mail.username对一个的值8 T0 k6 k9 a% g1 m

/ Y4 D# z9 a# T重写了toString方法,一会测试时候可以看效果。- e) M, f" c7 [
! h: {1 C" ]. N9 `: N& e& F5 ~
再来个普通的bean,内部会注入MailConfig: v! H4 q) }2 j% @3 w

2 u" @8 C/ w2 Y8 k" {package com.javacode2018.lesson002.demo18.test4;" u" ]" Z/ l% y6 f4 m
. Q4 ?/ n, ^9 Z4 U5 ^. w
import org.springframework.beans.factory.annotation.Autowired;
- {) `! J0 `7 m% s& x+ ximport org.springframework.stereotype.Component;
; T: t) I, S1 x. I! B+ G) g% T# A
@Component
" q, K$ c+ `6 g6 H1 [public class MailService {3 s& Y4 c, Z; _7 |2 b
    @Autowired) f( F4 m  d/ n5 R& ^' L
    private MailConfig mailConfig;
' l4 A8 h7 [+ v0 u
- d) a% W7 K; a) ~    @Override, _/ q/ ?: o8 U. {' G; q4 b( f) P+ s
    public String toString() {
; w9 e- G6 t9 k        return "MailService{" +
7 m$ k( b; ?; k                "mailConfig=" + mailConfig +
  T, d3 _' N  q! u0 [                '}';; i$ w7 O2 L3 V$ F8 p, w7 n7 p; R
    }4 F3 [; G3 s! B; G
}: G, R; u9 m0 \) b6 c
代码比较简单,重写了toString方法,一会测试时候可以看效果。* `$ A4 ~) y; X2 Z8 i

' `( y$ i7 r: g* M3 w+ s2 [# A& B2 J来个类,用来从db中获取邮件配置信息
- [# q* K8 c+ [" |3 Q* K
! [, m$ n" j; [2 Spackage com.javacode2018.lesson002.demo18.test4;" a" o) |) [0 ~

& M) Z6 ?5 d: K. h5 z! u- [$ h9 Ximport java.util.HashMap;- c5 I; P5 s: M+ Z$ B
import java.util.Map;
' [7 p$ l8 @: m) Z  {$ O& pimport java.util.UUID;8 v: j7 i2 F7 ]: y/ N! Q: w6 p
. g( |: R/ _5 M5 w" c$ e
public class DbUtil {
9 ?# l1 s) `2 T* v    /**( `. R: L5 U( [8 u
     * 模拟从db中获取邮件配置信息' s  n4 p5 T7 O2 R1 Z% [6 ]
     *! S) h3 f3 M+ b, R
     * @return
- C) y9 _( |* m& `7 ]" a     */% O4 w3 f1 L0 f8 @- f
    public static Map<String, Object> getMailInfoFromDb() {: M  z! [9 ?7 k0 N1 c- k
        Map<String, Object> result = new HashMap<>();
; u: z5 g0 f" _' @1 L/ A        result.put("mail.username", UUID.randomUUID().toString());5 U! C2 ^4 y5 e  y) i- a! T
        return result;4 ?$ G+ t1 R+ T0 i8 Y2 Q) g
    }
* A' v+ |! z( j$ o5 B}) _/ _  h% e: G( p1 l2 X, r' f) s9 N
来个spring配置类,扫描加载上面的组件
; A& M( v9 p) }2 i: G  X: L8 ^1 \6 i! R( o/ h
package com.javacode2018.lesson002.demo18.test4;* T  K" E  K$ P* o* |8 ^
& `+ z, z6 w7 t9 }$ v
import org.springframework.context.annotation.ComponentScan;
, \, _$ Y7 _6 F9 r; C8 Mimport org.springframework.context.annotation.Configuration;: A  a9 T# I2 s4 ?

: [/ a: p& Z) i0 }/ `+ o$ ^@Configuration
4 u/ @% P9 z5 j+ H/ F' h@ComponentScan
3 C& t& I3 }* R* Q: p6 Rpublic class MainConfig4 {
1 r' o; a9 {0 p5 l, r* u% p}
3 y" H$ I7 z8 G; g来个工具类0 \0 F: _4 p* W  t5 ]& E1 e

- m% u* x1 J/ b内部有2个方法,如下:
# ]6 N& N' N7 u; o0 Z! x1 x5 T3 T, |3 @
package com.javacode2018.lesson002.demo18.test4;+ L4 D" X- r3 B- K. X, L0 v* W  M& c

/ X3 l* q. q& Z2 V: f8 L. M1 Aimport org.springframework.context.support.AbstractApplicationContext;
. p" S' L( T- p! timport org.springframework.core.env.MapPropertySource;
) E1 g: b1 [( \; J/ C6 E  V7 c* B! n% ?$ @! H
import java.util.Map;1 L9 [" v1 @  }2 i# a2 Y- r1 Y

, t$ ^( s8 Z* r4 x. u* y: f% upublic class RefreshConfigUtil {
+ _- J( E0 z& {0 \0 p5 W    /**
* {$ [  _+ _3 ~8 x7 H% |9 F4 |3 U     * 模拟改变数据库中都配置信息
7 L- ^+ G0 S+ m     */. T9 V/ J5 ]" Y
    public static void updateDbConfig(AbstractApplicationContext context) {  [- K  S3 E) I% u
        //更新context中的mailPropertySource配置信息
7 }( T  i& t1 i3 @6 v        refreshMailPropertySource(context);
4 R5 ~9 S( y7 C
( h% C( m* Z5 h: l' ~1 U, v        //清空BeanRefreshScope中所有bean的缓存$ w" H9 s  ^. T1 F$ o" x0 v
        BeanRefreshScope.getInstance().clean();% g; W% B0 ]) `! d
    }
6 J9 x" V6 h$ |' `* W- {9 }$ V
- j# z" i, j5 O* D    public static void refreshMailPropertySource(AbstractApplicationContext context) {
6 }8 ~. v8 `. E3 a+ K, Z        Map<String, Object> mailInfoFromDb = DbUtil.getMailInfoFromDb();
( g3 ?4 ^! z3 P: L7 D) a/ I8 @5 K        //将其丢在MapPropertySource中(MapPropertySource类是spring提供的一个类,是PropertySource的子类)
: l4 J# F6 {  O+ O3 M/ v( P$ X        MapPropertySource mailPropertySource = new MapPropertySource("mail", mailInfoFromDb);
5 X6 l+ l4 ~& H" O4 D/ _- j        context.getEnvironment().getPropertySources().addFirst(mailPropertySource);( B- Q1 s; C: g
    }
: P4 ?' v3 B8 F3 R3 B2 N
4 o* ^2 G' I, w3 E. |}* a1 B. f0 u4 k* k% S# A' v
updateDbConfig方法模拟修改db中配置的时候需要调用的方法,方法中2行代码,第一行代码调用refreshMailPropertySource方法修改容器中邮件的配置信息
7 U3 D# `2 P. l
8 k, F9 t- J8 j/ i  hBeanRefreshScope.getInstance().clean()用来清除BeanRefreshScope中所有已经缓存的bean,那么调用bean的任意方法的时候,会重新出发spring容器来创建bean,spring容器重新创建bean的时候,会重新解析@Value的信息,此时容器中的邮件配置信息是新的,所以@Value注入的信息也是新的。/ k+ [* E- Y2 f3 w, c& M
  }. X, i, P; Y- O$ K
来个测试用例6 D# ]" A0 x# j4 u6 Z, A, `% |& K

$ w8 d& {: b  k, k6 P) h@Test) B& ~! t8 d- w* z4 l* x: @) u
public void test4() throws InterruptedException {' c* T" u2 k4 s; R7 t4 I  X" \
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();$ f8 X4 _! l: s# J
    context.getBeanFactory().registerScope(BeanRefreshScope.SCOPE_REFRESH, BeanRefreshScope.getInstance());* c6 `9 E& M: B' \6 m
    context.register(MainConfig4.class);
, n8 Y& s6 D' h5 Q( _    //刷新mail的配置到Environment+ r& I4 k) y1 b: }5 i9 }
    RefreshConfigUtil.refreshMailPropertySource(context);
+ r2 ^& q6 \* [: R& ^    context.refresh();- `& _8 ]1 {/ V+ m* J

5 O: S& i4 ~# l    MailService mailService = context.getBean(MailService.class);& F5 B. R, G$ \6 Z" c/ f7 ]- _
    System.out.println("配置未更新的情况下,输出3次");3 M* q+ e" Z$ m( b8 U; I1 M
    for (int i = 0; i < 3; i++) { //@1
3 N; _9 a+ q/ z. ~6 K        System.out.println(mailService);
+ h: |( A1 i& I& B* M* f* Y6 m  Q        TimeUnit.MILLISECONDS.sleep(200);
$ T3 N4 a0 Q5 S; ^2 V$ O    }
1 _: P6 m5 y) t& z8 H3 K; w" s. ]- j
    System.out.println("模拟3次更新配置效果");/ m0 z9 ?  t8 n4 }! t: V
    for (int i = 0; i < 3; i++) { //@2
7 o( q/ @$ X. J! t7 S) d7 o% x        RefreshConfigUtil.updateDbConfig(context); //@3
9 K) T/ x3 [! {, Z! k/ l" A        System.out.println(mailService);& Q% Q+ _* n1 K
        TimeUnit.MILLISECONDS.sleep(200);
' X* u! d/ i! A    }
; q  R4 e; b/ \" \}/ G# z4 [  w5 N6 S6 R3 [# x! S
@1:循环3次,输出mailService的信息! S; i# u3 b* e8 t( T/ _

8 W0 l' O& m1 I! A& Y1 n@2:循环3次,内部先通过@3来模拟更新db中配置信息,然后在输出mailService信息
; y6 v# F/ Y3 |1 |5 C, h$ y  K3 i2 d0 K4 B. A/ ^, q
见证奇迹的时刻,来看效果
- x: P& {8 D& `# Q3 b+ [0 b' H
+ X' H  r8 T7 ]9 G0 j配置未更新的情况下,输出3次
- }1 }/ O# b$ }" x  W9 l' T0 hMailService{mailConfig=MailConfig{username='df321543-8ca7-4563-993a-bd64cbf50d53'}}
7 a% m3 P$ Q/ C: LMailService{mailConfig=MailConfig{username='df321543-8ca7-4563-993a-bd64cbf50d53'}}
5 L$ E- W- C& X% L7 }MailService{mailConfig=MailConfig{username='df321543-8ca7-4563-993a-bd64cbf50d53'}}! o! Z8 r8 f1 Y) p. W
模拟3次更新配置效果
  d$ i' J2 N4 z! i, QMailService{mailConfig=MailConfig{username='6bab8cea-9f4f-497d-a23a-92f15d0d6e34'}}
6 c# C" N; ?, ]9 B2 L+ M0 G, RMailService{mailConfig=MailConfig{username='581bf395-f6b8-4b87-84e6-83d3c7342ca2'}}/ Y  \7 S- i, q& j3 m
MailService{mailConfig=MailConfig{username='db337f54-20b0-4726-9e55-328530af6999'}}
& ?2 @$ [0 K6 ?7 e上面MailService输出了6次,前3次username的值都是一样的,后面3次username的值不一样了,说明修改配置起效了。: S5 b. J# i7 y3 S! W
- \" B7 z3 O7 ~; F9 l% L8 M* [
小结( ]/ H1 N* O: F; j% L( k. n1 t  j

+ E; ?" x! d- }+ J动态@Value实现的关键是@Scope中proxyMode参数,值为ScopedProxyMode.DEFAULT,会生成一个代理,通过这个代理来实现@Value动态刷新的效果,这个地方是关键。
; B% T& x( @  ^3 s9 _1 y! X
, {$ q$ H1 b  h$ v% x( g* _有兴趣的可以去看一下springboot中的@RefreshScope注解源码,和我们上面自定义的@RefreshScope类似,实现原理类似的。
: b) Y3 F$ M7 J. X3 ?& P1 ]# D
% o/ A6 c' E1 E9 H总结
% }! i% }: P0 e) a5 e. X2 y! m! w+ Z2 q, L0 o4 G1 m$ y
本次面试过程中3个问题,我们都搞定了,希望你也已经掌握了,有问题的欢迎给我留言,交流!
: Y% y( ~" z. _- _1 ~# ^7 y+ r! P3 E* i9 Z/ K
案例源码: j; ?/ O0 ?9 G% a  p# W" K2 F, E
: z9 c+ ^3 g: d+ Y) }/ @
https://gitee.com/javacode2018/spring-series
+ S- J: T  {: ~4 f路人甲java所有案例代码以后都会放到这个上面,大家watch一下,可以持续关注动态。
5 ~7 N) X8 ^* @/ C& m
8 Y  [9 F. F- L! fSpring系列7 ~" H' \. j' X' G
; H! b: |+ O% v8 P  b. s
Spring系列第1篇:为何要学spring?
0 A/ R: J2 ^  B1 K2 i
: [. D4 Z4 i% G4 RSpring系列第2篇:控制反转(IoC)与依赖注入(DI)
6 U/ [6 o' N9 f- y/ D
9 n7 t5 L' Z& ASpring系列第3篇:Spring容器基本使用及原理2 b5 M0 S7 A3 w3 A0 T2 l' `
/ k& u5 Y- m6 ?, O, K3 R
Spring系列第4篇:xml中bean定义详解(-)
" ]: g" l4 z# H# e/ L9 y1 x. D" r7 `$ K/ C0 ]7 D4 Y
Spring系列第5篇:创建bean实例这些方式你们都知道?, L+ p9 f+ D: Y( f
& x+ z/ y# ~- E% f& Z0 ?
Spring系列第6篇:玩转bean scope,避免跳坑里!; J0 X- l9 V  E- [5 P
( S5 ~* X8 |9 ]7 M" m7 c8 T
Spring系列第7篇:依赖注入之手动注入: w$ D, W, j8 j" i' H
, x3 a, E8 [1 s. V" Y* h- d
Spring系列第8篇:自动注入(autowire)详解,高手在于坚持
$ ^: A1 B$ m' r" C  ^6 i) {
- J% }3 @, G  P! `Spring系列第9篇:depend-on到底是干什么的?% _* G1 v( X# \- N

+ W1 C* y2 \0 L- C0 [Spring系列第10篇:primary可以解决什么问题?3 }' E4 n6 P* Z: q$ {

3 j2 Z8 O; A% s8 k4 |# o, e- NSpring系列第11篇:bean中的autowire-candidate又是干什么的?- B+ V$ q0 U: G: r! R; K; a3 f; I

5 f( x3 x8 B. M' M, W8 K* ESpring系列第12篇:lazy-init:bean延迟初始化
- F2 n+ J7 u% V& T5 N
2 j; {7 Y0 n3 bSpring系列第13篇:使用继承简化bean配置(abstract & parent)
# p1 t$ Z0 E4 j! ?
$ R9 E8 J3 Y+ J$ G2 Z- rSpring系列第14篇:lookup-method和replaced-method比较陌生,怎么玩的?
5 {9 z  \2 ~9 P% Z8 {& U# H0 D6 i
: i  u. C0 ]# I% X' k+ n6 k, TSpring系列第15篇:代理详解(Java动态代理&cglib代理)?
" t! q4 i4 J+ n5 v% k* M/ x& R4 H- U
Spring系列第16篇:深入理解java注解及spring对注解的增强(预备知识)
# ]- a( }- `0 z6 a/ y* m- l! k) j0 [) B: e* N0 ]
Spring系列第17篇:@Configration和@Bean注解详解(bean批量注册)
/ k3 ?+ [( l( i# t
) M1 W/ k+ {) p' F6 X2 q1 pSpring系列第18篇:@ComponentScan、@ComponentScans详解(bean批量注册)8 F  f) h2 H  Z8 S/ S

9 o& ]# e; }4 BSpring系列第18篇:@import详解(bean批量注册)8 I% p" e# ~3 f6 ~* P! |6 L, p  i

$ E: C8 y8 C( H- I. N) u; z& hSpring系列第20篇:@Conditional通过条件来控制bean的注册; [. `4 Q" F! D' m6 A; K# Z

0 {& y+ G! _! e5 `: C. \Spring系列第21篇:注解实现依赖注入(@Autowired、@Resource、@Primary、@Qulifier)
$ [7 g  W: r# i
1 Y- K3 R4 u' d$ p/ HSpring系列第22篇:@Scope、@DependsOn、@ImportResource、@Lazy 详解. K) i* C% R: M( k4 l- u( k2 v5 Z8 |

7 w9 C, [' }) k6 \" jSpring系列第23篇:Bean生命周期详解1 H) Z9 L6 f7 R; _& `5 {/ Z- |

1 ?* o! }8 G6 QSpring系列第24篇:父子容器详解
5 B( n, ~9 R  X! m* u* u: {, Q& I, C. K8 \! X+ Y3 [
更多好文章
8 f7 i  q* ?  I! O, s8 x" ?1 v% _3 U! N7 r$ z7 X* H& Y
Java高并发系列(共34篇)6 \" h1 K+ p/ V/ P* n3 U

$ y  u  d8 ]$ x, G. P+ \MySql高手系列(共27篇)
. D9 e) s+ _  H
9 }* X& g3 v$ ~0 M, sMaven高手系列(共10篇)" p! R, _, A7 T1 \4 Y; Q- f9 K

5 M/ b" N- H2 e6 S( K; K; [" NMybatis系列(共12篇)9 ~9 ^; Z' G9 J8 ^' R
7 q# t2 t0 d2 q
聊聊db和缓存一致性常见的实现方式
4 F, Q/ W; x& \! g  l, `7 h/ ?6 m8 B- G- N$ c4 K3 Z
接口幂等性这么重要,它是什么?怎么实现?
$ n3 M% E' v5 Q
6 k* s, ^2 K6 I泛型,有点难度,会让很多人懵逼,那是因为你没有看这篇文章!5 O3 m' |( @: A: |! l0 ?) B! Z3 c
————————————————+ J& {) k( c" i0 F! T) j+ }8 @/ L/ P
版权声明:本文为CSDN博主「路人甲Java」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
2 S; N) H' i& |0 f* K) e原文链接:https://blog.csdn.net/likun557/article/details/105648757& \# _5 p! w' h( V

3 q! Z2 ~( _& _7 `" C- ?8 s8 z+ m) Z! V- K. r





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