- 在线时间
- 1630 小时
- 最后登录
- 2024-1-29
- 注册时间
- 2017-5-16
- 听众数
- 82
- 收听数
- 1
- 能力
- 120 分
- 体力
- 568612 点
- 威望
- 12 点
- 阅读权限
- 255
- 积分
- 175806
- 相册
- 1
- 日志
- 0
- 记录
- 0
- 帖子
- 5313
- 主题
- 5273
- 精华
- 3
- 分享
- 0
- 好友
- 163
TA的每日心情 | 开心 2021-8-11 17:59 |
|---|
签到天数: 17 天 [LV.4]偶尔看看III 网络挑战赛参赛者 网络挑战赛参赛者 - 自我介绍
- 本人女,毕业于内蒙古科技大学,担任文职专业,毕业专业英语。
 群组: 2018美赛大象算法课程 群组: 2018美赛护航培训课程 群组: 2019年 数学中国站长建 群组: 2019年数据分析师课程 群组: 2018年大象老师国赛优 |
太狠了,疫情期间面试,一个问题砍了我5000!
4 j1 u: n, D8 f/ V8 y疫情期间找工作确实有点难度,想拿到满意的薪资,确实要点实力啊!$ U8 l7 x1 |; I/ z: V1 }% I$ r7 U
' t8 ^8 Z( d$ M/ N9 T' V面试官:Spring中的@Value用过么,介绍一下
' ]" @! M9 {7 ]2 N" v- u6 r+ K
& g% k k9 n$ m我:@Value可以标注在字段上面,可以将外部配置文件中的数据,比如可以将数据库的一些配置信息放在配置文件中,然后通过@Value的方式将其注入到bean的一些字段中
' t8 @/ l/ P8 B8 A" O4 u- A
& a2 ~! p' s+ y6 L面试官:那就是说@Value的数据来源于配置文件了?
- E: [ U, F; d9 N' L1 v
; h& r; w3 V' D; L我:嗯,我们项目最常用更多就是通过@Value来引用Properties文件中的配置2 Y) [% f; z1 F% M
4 I% u* H" E- Q' k+ O& u u* @/ \面试官:@Value数据来源还有其他方式么?; ?' n8 c8 { F
* f- a$ I- i, o) Y我:此时我异常开心,刚好问的我都研究过,我说:当然有,可以将配置信息放在db或者其他存储介质中,容器启动的时候,可以将这些信息加载到Environment中,@Value中应用的值最终是通过Environment来解析的,所以只需要扩展一下Environment就可以实现了。
/ {+ b" O0 O# `7 k( X
) h( a- J1 X6 J' z Z* q6 ]面试官:不错嘛,看来你对spring研究的还是可以,是不是喜欢研究spring源码?4 p+ Z# H+ y O7 ~/ M: g. m
/ ?$ \$ |0 o- ^6 J
我:笑着说,嗯,平时有空的时候确实喜欢捣鼓捣鼓源码,感觉自己对spring了解的还可以,不能算精通,也算是半精通吧% v) k, @6 C c2 F: _9 Y
+ L% W4 @0 o+ ^2 ]0 X/ ~面试官:看着我笑了笑,那@Value的注入的值可以动态刷新么?6 t0 H1 n1 T7 k1 t
- p, U# N9 A4 L/ p) q7 [
我:应该可以吧,我记得springboot中有个@RefreshScope注解就可以实现你说的这个功能
' H) H3 v" d/ w) j
) P" @' h' y. x6 Z面试官:那你可以说一下@RefreshScope是如何实现的么,可以大概介绍一下?9 S0 N6 C/ K% k
+ y# d* E- W+ t2 C' y# j# s6 B/ {
我:嗯。。。这个之前看过一点,不过没有看懂
9 s; @) r% }1 B) |" M z$ ^. h5 e8 S6 Q& ]- X' M( l) j, B
面试官:没关系,你可以回去了再研究一下;你期望工资多少?* l, f6 n* l4 c$ P( V+ `% s
' [6 ?) G, Z& U9 K$ ]
我:3万吧; E1 m* }! x. S$ n) c1 M9 S2 X; s/ X
: P! W' b' o1 e5 i
面试官:今天的面试还算是可以的,不过如果@RefreshScope能回答上来就更好了,这块是个加分项,不过也确实有点难度,2.5万如何?' q' N; S7 U" }) [) _' t- B9 P
/ Z7 {+ m6 Q& `' s% t3 `, I0 ^
我:(心中默默想了想:2.5万,就是一个问题没有回答好,砍了5000,有点狠啊,我要回去再研究研究,3万肯定是没问题的),我说:最低2.9万
1 I; p! ?. }( s* p t/ I1 }* O: ?# O* w) \# o( Y
面试官:那谢谢你,今天面试就到这里,出门右拐,不送!
. r3 a! F7 t. F( \. z) n0 R% o' W& [) w0 X7 T
我有个好习惯,每次面试回去之后,都会进行复盘,把没有搞定的问题一定要想办法搞定,这样才不虚。4 D* M/ u. R S7 d
: p2 c1 T9 {& ~' x. ?3 H
这次面试问题如下
0 z/ Z a* I& | ?8 H; E8 z0 b( V, f, |6 }, r# b8 H$ t
@Value的用法3 B+ s2 h8 m' d8 |* M7 Y1 f
2 {, ?5 o6 B6 k+ J- N
@Value数据来源6 C% _( \% y7 ?/ o7 r1 ?/ L6 o
, s7 ~( }7 \; R' |
@Value动态刷新的问题
7 y6 B, L$ ~9 K6 n) {2 v
$ j: m+ v8 M5 j0 W' ^# `( D7 J下面我们一个个来整理一下,将这几个问题搞定,助大家在疫情期间面试能够过关斩将,拿高薪。
5 z. l5 f, W8 }7 J. a
7 g* C( x: G$ Q9 S: S7 [0 W@Value的用法0 D" p' R" g; M3 E
. d" s! y7 T* l; R$ c- P系统中需要连接db,连接db有很多配置信息。
% ? I, b: Q! r8 D3 p5 J
: q9 X, M. M! B/ q/ I# r6 ^' P( h系统中需要发送邮件,发送邮件需要配置邮件服务器的信息。
4 E, ?' C0 C0 [7 b8 `. C
( k y: A0 w' K; z还有其他的一些配置信息。
+ S9 V9 e" S- P, G. p2 }# _1 u! K
! C% d: y. f& n! {6 U$ n4 P( D我们可以将这些配置信息统一放在一个配置文件中,上线的时候由运维统一修改。
$ G. K- R0 M* z2 |
' H5 L( T* @2 T2 J& ?# W' Z; M那么系统中如何使用这些配置信息呢,spring中提供了@Value注解来解决这个问题。
; c5 U" J0 [" Z+ S1 N8 b5 E! }7 g6 I ^! s: \7 |
通常我们会将配置信息以key=value的形式存储在properties配置文件中。
# a" r( Y4 P& m3 i: V. d
$ B: i9 F5 h/ S7 U: c通过@Value("${配置文件中的key}")来引用指定的key对应的value。/ _* ?& Z! {/ S, `7 ~
/ K7 F! f2 k7 l m@Value使用步骤
7 H) M$ M0 c. b8 b I- p$ x7 R
: y1 d8 r' t; o6 {7 @步骤一:使用@PropertySource注解引入配置文件
. B5 o/ i6 R7 e
! Q! b$ Z+ E6 R/ H将@PropertySource放在类上面,如下; K3 @! @0 Q1 p0 [: |) h% Y$ Z
$ b, k5 M7 k! v& N( }@PropertySource({"配置文件路径1","配置文件路径2"...})
5 f+ V- }* E; S1 U7 D+ ^+ q@PropertySource注解有个value属性,字符串数组类型,可以用来指定多个配置文件的路径。1 H8 e5 a0 d/ L- z( Q
. b/ I( a1 m1 K
如:9 c6 a; u+ h. D& K# e& V, l- r e
0 N' [0 O- A. h
@Component
3 x" B# R" A4 M+ i@PropertySource({"classpath:com/javacode2018/lesson002/demo18/db.properties"})
# |. Q% J2 F x+ x4 spublic class DbConfig {
; g) a B8 J" L$ u" o5 D# i# V}
& d& X, l* B- |) `- i# b, a; k步骤二:使用@Value注解引用配置文件的值( C3 p% u) n' b( H3 |
5 q- L. p! Y0 o0 }- a3 l$ x+ f通过@Value引用上面配置文件中的值:
8 u$ ^1 p1 U( j
4 H$ c8 G' W, Y4 k语法- X- K" U7 @" p" D) U' F
) M, _+ ?7 Q1 N$ W8 ]- l@Value("${配置文件中的key:默认值}")
% r4 W$ F0 L: _ p4 d' \- \/ l! ]@Value("${配置文件中的key}")% ]" l% I3 a1 o D5 _9 T- B
如:
Y" [' Z; K- I1 V" _6 y( b( b. `# N) H
@Value("${password:123}")
. S7 k6 ~5 O9 k+ [3 A3 G7 k上面如果password不存在,将123作为值
- x9 [6 N# C9 U7 E
( n$ p6 E# ^3 \2 f2 B@Value("${password}")! }8 h Y5 v" E o" z
上面如果password不存在,值为${password}9 J$ n5 G8 ? G
% D0 U: M G% A- O0 }, o
假如配置文件如下
+ F! ?9 f) @' b. C1 u1 i! q9 }! R! d6 B [6 g) c2 c
jdbc.url=jdbc:mysql://localhost:3306/javacode2018?characterEncoding=UTF-8
3 [2 W5 }! U& q5 Cjdbc.username=javacode( s; ^+ m. h7 ^5 R) i' A, O0 l7 c
jdbc.password=javacode6 a `) B6 M0 s
使用方式如下:% I' n( J9 X# V N8 d
5 j1 G3 I7 K w1 V7 Q@Value("${jdbc.url}")
/ [% I6 w( A9 _+ U& N! wprivate String url;. f/ F9 h" D, V- l& H6 H' f
* U2 j3 }2 o6 S3 m8 `7 k2 G@Value("${jdbc.username}")
* H+ {, J* ]' z8 L( M* dprivate String username;" l; L& l/ t( F8 c2 J2 _ x g
- K7 D8 G3 S( {: `1 c@Value("${jdbc.password}")
1 M, h( m) Y7 g# z4 S$ z" jprivate String password;9 v$ r8 D1 a; V6 W9 G$ \
下面来看案例
) d' E" x" H: D! l& b1 Y2 x( u" q8 j5 {
案例/ {, u+ X$ ~! C$ d
/ ]9 V0 a" _; j
来个配置文件db.properties4 A5 y8 x6 d0 _% }1 `
K9 g9 L, d) Hjdbc.url=jdbc:mysql://localhost:3306/javacode2018?characterEncoding=UTF-8
! F7 b6 Y2 i, f0 z- G9 }; z* G7 ~jdbc.username=javacode
0 d$ N t7 N# N+ \! Ljdbc.password=javacode4 m% a" d* F1 m! N
来个配置类,使用@PropertySource引入上面的配置文件0 b( _1 e, f0 L3 q; C: g# ]
# [1 R" [7 g& X) J9 D- A' Ppackage com.javacode2018.lesson002.demo18.test1;
3 E. c. t" ^# j0 ?/ h7 y! ?! @9 f6 r& N! S( f4 z+ B2 \' h
import org.springframework.beans.factory.annotation.Configurable;- \5 W, |$ x! j9 n0 u6 p9 K
import org.springframework.context.annotation.ComponentScan;
; s1 X' ]1 l3 ~ k* ~$ ]; iimport org.springframework.context.annotation.PropertySource;8 }9 f2 k8 P# M$ |. l' M5 _
4 `% w7 n5 x; F" |/ m4 {@Configurable
& Y7 @9 f2 n* B- \@ComponentScan
2 b% W4 B9 Q0 A2 v3 h# T. P@PropertySource({"classpath:com/javacode2018/lesson002/demo18/db.properties"})$ n$ Q6 `% b4 g$ d8 Y Y$ F/ X
public class MainConfig1 {0 g/ u! z# y9 z7 ]& |$ O8 w5 {
}7 `& P4 V/ e2 f! [, x' I6 `
来个类,使用@Value来使用配置文件中的信息- `+ Z' z5 o4 _7 g
6 h$ f( x9 Y, z# [1 p
package com.javacode2018.lesson002.demo18.test1;
% \/ ~/ {* Q+ R5 z; l
. p% u6 M1 Z; N. gimport org.springframework.beans.factory.annotation.Value;
1 z8 c: {6 i. X; j) @import org.springframework.stereotype.Component;8 V2 O9 j% B- t( l5 S
8 i, L% @& `" ~5 C2 ^+ |@Component2 i i4 S9 x" |: s
public class DbConfig {' A7 i3 b2 g+ q4 b) z3 \
1 e( p" T0 f, @3 x( S. l
@Value("${jdbc.url}")6 @" d. V2 t7 X) k! _, P8 t2 x
private String url;
& t; e( W; C G2 `3 {
4 I% a! O5 Y% _ F2 ~ @Value("${jdbc.username}")
D( \8 M+ z8 `2 M& g" z: A2 @ private String username;
9 N8 c+ f2 K3 q# u" a$ W, e8 Y; B1 g' z
@Value("${jdbc.password}")7 D0 F6 ~ _. X8 H( i0 A/ a
private String password;8 V: _5 x; W/ Y! X& u& a; }
4 E' P% G- u$ S4 X public String getUrl() {
( _4 k1 c6 g6 Y: ` return url;
) x1 k2 [/ `0 |' m& [ }
# y, a* \ F' A; m& f/ S3 c$ o! S" |# X$ g- L. |% \6 _
public void setUrl(String url) {. d' o0 w3 a' o7 r) U9 R
this.url = url;
1 E" @( o9 }1 C }
, }3 b" [! @! m! D/ V* m$ v2 [/ a/ {# @
public String getUsername() {8 K7 s$ ^, L: n4 U l
return username;
3 d+ c" Y& |+ }9 z }+ q: p0 O' j6 A9 i- v
% Y3 V: b6 N4 B( r8 J public void setUsername(String username) {
f, ^( A% n- D- i3 U this.username = username;
! G, W5 L i% b- ]4 d5 R5 F }0 Q: t1 H2 a% X" ]! O' T8 M) V
% r* n4 |! C2 p. \4 m public String getPassword() {
- S) I9 t6 Q4 v% |. F) K2 X8 o+ V8 T return password;' f: A2 i- Q$ y- O& j- c
} @6 M( W. `# K% S8 z* o. S
; L; w! D" s( ` public void setPassword(String password) {
. z' m+ L( G* c1 k3 l this.password = password;
, ^/ e% u4 r+ E# k/ _9 C" S }
+ K9 k0 w+ o* x$ i
) V( S l- w) r2 B) C ~0 I @Override' j1 v" V7 A0 v y# j. o6 L
public String toString() {0 K0 v% R) k) k, C, k/ i" E3 W
return "DbConfig{" +
3 z- P3 f' ?2 s$ n3 e7 C "url='" + url + '\'' +5 z0 M5 y& O% f ~5 Z, J, q8 w
", username='" + username + '\'' +- _* d- C4 d8 T; M. i
", password='" + password + '\'' +
# t0 g) K" ^! G& ~3 ~$ x9 o( R% s- G '}';' S' e) y5 K- w( {7 i+ Y- Z# w
}
6 _& f+ s- b. g' n2 l% R* T: C. A}
2 J1 F- v3 |9 G: m' g. R$ J* Z上面重点在于注解@Value注解,注意@Value注解中的* m( c L7 |9 `5 R8 {
: d0 H- k) e% j; y$ O; I# z7 ^1 x来个测试用例, F& w% h5 G# r* c
6 k- O$ F4 K+ S: g# g0 l
package com.javacode2018.lesson002.demo18; ^4 \" P0 Q- _4 d: K. {
6 {; ?+ L. V2 m' o( ~2 p7 X
import com.javacode2018.lesson002.demo18.test1.DbConfig;$ X. h2 `+ q! N* s: ]4 L
import com.javacode2018.lesson002.demo18.test1.MainConfig1;
" X2 Y! r% ~* O7 b' g5 ^import org.junit.Test;$ @" |! A# D; A0 v+ \. J
import org.springframework.context.annotation.AnnotationConfigApplicationContext;5 O6 C4 N6 h" x
: L4 v4 f5 Q8 I+ S/ K/ W
public class ValueTest {
% i& \8 i- e6 n' L5 l V" n; [' W, Y+ I, l1 W
@Test
# o( L2 x' r& u; N/ Y public void test1() {
( S$ f4 |) p& ? AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
$ p- ^4 x' i& v, u. W9 g context.register(MainConfig1.class);' ?! ]+ E0 p5 {+ p1 P" r( `
context.refresh();
3 S1 \ J b& z8 ^" O& W. t2 e
$ k* e# ]2 r/ z0 D; i DbConfig dbConfig = context.getBean(DbConfig.class);1 p; D2 V8 \3 i% W7 o- g0 w
System.out.println(dbConfig);7 X) c# r$ T" d9 h2 J7 t
}
9 X3 \/ y+ Z1 |& r* C}* t# V% C! X* {/ F3 Z
运行输出# A/ |) d, r; }) {
+ j2 P2 M$ [# X( h; s7 ?
DbConfig{url='jdbc:mysql://localhost:3306/javacode2018?characterEncoding=UTF-8', username='javacode', password='javacode'}" M* q6 X, P8 P, x* c1 }5 B
上面用起来比较简单,很多用过的人看一眼就懂了,这也是第一个问题,多数人都是ok的,下面来看@Value中数据来源除了配置文件的方式,是否还有其他方式。$ T5 V6 M8 S: J, C% R. f
2 d y# A: y1 e! F W$ j0 ]$ P@Value数据来源 B5 s' i, X4 K$ Y# l
) I) [1 Q- \+ Q
通常情况下我们@Value的数据来源于配置文件,不过,还可以用其他方式,比如我们可以将配置文件的内容放在数据库,这样修改起来更容易一些。7 V. v. s C8 I. p1 \4 I
2 B2 T% E; U4 V5 r0 v0 r+ [
我们需要先了解一下@Value中数据来源于spring的什么地方。
" a/ x& |: J! \+ p! |5 W0 Y0 [3 ]% Z [" v. f! }% M) k
spring中有个类& |; r1 `* Q0 Y0 j2 ~' X+ v1 B
1 k- h! [, g, ?# {* v, G% V8 X3 ~
org.springframework.core.env.PropertySource, x, t' T6 F' y# p. j& [' m8 Q
可以将其理解为一个配置源,里面包含了key->value的配置信息,可以通过这个类中提供的方法获取key对应的value信息1 ^' C- h7 ?5 @5 p4 D! A }/ t
/ q- O9 a; j- o' A m# K$ \
内部有个方法:
; ]$ I% |4 B7 f: w1 j3 U! a$ @
5 @. R4 B% o' r3 Kpublic abstract Object getProperty(String name);2 F) ^. L2 G1 ?
通过name获取对应的配置信息。% F) f4 P' R4 {8 T
3 J7 Q1 C9 g8 E% i. d& f5 I系统有个比较重要的接口, o5 Y5 _- e6 j8 s
) R% W3 s, A, L8 Eorg.springframework.core.env.Environment' \$ w/ e0 r6 l# ~' g% c' ]0 L$ [
用来表示环境配置信息,这个接口有几个方法比较重要
. ]0 h3 G) i$ e# f! n9 P L4 X+ `- s" W ?$ e6 s4 p) G' n
String resolvePlaceholders(String text); z: n9 h1 U; Q9 D$ Y' O
MutablePropertySources getPropertySources();5 E* j3 ?7 k' f7 I+ a. Z
resolvePlaceholders用来解析${text}的,@Value注解最后就是调用这个方法来解析的。
3 L3 h1 W7 F3 O1 X6 i0 A Y2 [9 P% w: L4 w7 m/ N
getPropertySources返回MutablePropertySources对象,来看一下这个类
/ l# \+ r8 g; z2 k- D2 o; p8 Q
" e: Z7 E: b* _- w) K& U2 Q, rpublic class MutablePropertySources implements PropertySources {
* G8 w+ U$ M: S; E: v6 P# ~7 ~- s- J- q! f, M. E& Y7 t- F+ y! u5 Q
private final List<PropertySource<?>> propertySourceList = new CopyOnWriteArrayList<>();$ k) W5 T0 M0 Y* k( f+ J! L
: V1 b. ^4 \& Z' \! ^8 B}$ K2 n; H1 ]0 [5 j1 v
内部包含一个propertySourceList列表。
Y7 Z9 i$ |6 E y6 x6 t4 S, p5 p1 z, c# m% P1 x8 V
spring容器中会有一个Environment对象,最后会调用这个对象的resolvePlaceholders方法解析@Value。+ S7 v b8 L1 r
- S% G }' l+ `4 q. U大家可以捋一下,最终解析@Value的过程:
4 X0 t' {* N* e* J6 d' O8 v' `6 H% d# N0 H
1. 将@Value注解的value参数值作为Environment.resolvePlaceholders方法参数进行解析
/ ?0 w# I# L7 G! x2 Z9 \2. Environment内部会访问MutablePropertySources来解析" y7 a; J$ D2 H% M
3. MutablePropertySources内部有多个PropertySource,此时会遍历PropertySource列表,调用PropertySource.getProperty方法来解析key对应的值' K8 e3 G7 w! a% T( U3 x; `
通过上面过程,如果我们想改变@Value数据的来源,只需要将配置信息包装为PropertySource对象,丢到Environment中的MutablePropertySources内部就可以了。, ^+ x# _2 ]$ T+ V& Z& w
0 Y3 x8 f q5 N: w' a3 m& U
下面我们就按照这个思路来一个。
0 ~* a2 P7 E9 b* t) k$ Z
3 U* r4 l" k- X b来个邮件配置信息类,内部使用@Value注入邮件配置信息% G) u1 _" A& L
4 j& \" |- g: M( N+ k2 z/ A( s3 V2 D
package com.javacode2018.lesson002.demo18.test2;, } ?7 ]! [, S( O" j7 V) v
; e3 r& f+ f: Kimport org.springframework.beans.factory.annotation.Value;
5 Y p% x5 |2 B& K0 W7 nimport org.springframework.stereotype.Component;# |# \# m/ Z( O) y. l+ O9 Y
2 v- {% v! q; A9 P4 ^1 S4 Q
/**7 {8 r6 N% [/ b/ i, l! U* L
* 邮件配置信息' k, Q% F- w+ ~/ l# Y1 Q3 |( [
*/
, j$ W- L5 C/ H* s@Component6 ^7 ~* i. r) w1 g) `
public class MailConfig {
( v! {/ Y: r9 Z: z" w l! D
` ]( C4 C5 N" l @Value("${mail.host}")
* |9 d1 j C* J$ ]9 V private String host;
: L2 c# M) f# d0 [9 B& R& m/ n0 q2 M
+ S! i2 p |7 V( ?. V @Value("${mail.username}")
0 {" S5 v& v. X R5 Y# t3 ~: o7 F private String username;- R" i) k- w! W* J/ ]9 T
' B$ P% g3 X3 s
@Value("${mail.password}")6 G- d: B" R& s- r8 W9 ^) b! }9 s
private String password;% N' G2 R. W, H/ l. M3 p% k
# \% U5 y# o2 @% }' y
public String getHost() {
4 h& Y8 A0 f2 A `$ d3 k return host;; |) E8 a5 g( @: }7 I
}2 J' Y: T4 L" }& D; f" x' L
, s% |' j- |9 _
public void setHost(String host) {5 G/ u7 m* x0 {0 E
this.host = host;2 L9 O R2 Y4 O1 ?) Q) K5 w9 u
}
3 d* ~7 q+ m) w: C: `) s
$ B8 r- ?4 ?$ Y" ] public String getUsername() {, }( s. Z/ M; P/ v
return username;( _5 B0 u- ~% n; j1 r" D
}9 k& W/ h! R; V! J
6 e1 H8 k0 J$ H$ Y
public void setUsername(String username) {/ \! L9 Y( j0 r; H2 n* O
this.username = username;" r/ x$ a3 [+ m/ \4 n9 K& E
}6 Y" S1 F' K" u$ J
- u, r- n; V1 L
public String getPassword() {
& p6 `3 v3 c0 A1 Q/ n9 U: ^. Z return password;% n3 V' `& x: W6 T8 x6 f
}
8 k1 T( y" {, {! P. d. L/ ^( L. ~. V+ z; F9 r- r6 [
public void setPassword(String password) {( B( P: [7 h( _& E# N6 g
this.password = password;- z" H& f7 A5 }( r
}
2 E1 D, G6 e: U. p" ]% s1 s" E2 ]! @' G8 J' u1 M1 I3 A5 L J
@Override
1 m4 \; |. z4 k9 W public String toString() {
# }) g/ A+ r# \) A return "MailConfig{" +. B, u, z& s. h1 ?6 j' m1 p' b$ ^
"host='" + host + '\'' +
6 i7 v* V# T4 l' P ", username='" + username + '\'' +
9 Z. C4 x# A; C9 z. J0 w: ^9 U ", password='" + password + '\'' +2 p% W' ~% ]5 T! Y! ?
'}';4 o% D0 ` P9 e" y
}
, O* r$ r9 I% K7 F" ^}' Y i9 g$ M# L+ ^3 K. v
再来个类DbUtil,getMailInfoFromDb方法模拟从db中获取邮件配置信息,存放在map中2 {( N; Y# a# Z0 [( o
0 H2 l' H' x0 |2 M9 L$ L" N" i
package com.javacode2018.lesson002.demo18.test2;2 s; m6 ~ L7 J, u# \0 V
; l3 d' |8 b; I% d+ }' ]5 R
import java.util.HashMap;9 R8 H5 j& C5 Y2 w8 E9 `
import java.util.Map;
$ b# w# ^* S% V- T& I
( `- U5 ?% h x; U8 E) {4 n$ jpublic class DbUtil {$ P9 {+ b% T2 b0 K o; d9 v( W# G7 _7 |
/**8 ?! v' k& V& R) g+ H
* 模拟从db中获取邮件配置信息
$ L2 A+ }( _' y' Y8 ^+ [/ @; m *, f3 H$ {# T% N8 `5 ?' V( q6 v- M
* @return# E! X8 r) _$ F, }
*/
8 ?5 }1 s# ]3 Y5 e public static Map<String, Object> getMailInfoFromDb() {: T! [8 |% |7 B+ [" _8 u m
Map<String, Object> result = new HashMap<>();- x I" u1 O w% T
result.put("mail.host", "smtp.qq.com");
* D5 E; T. m6 U result.put("mail.username", "路人");+ G& h8 p9 v# W! F, B
result.put("mail.password", "123");
?( H; |8 P2 Z0 Z( ` return result;
. V& X2 Z7 c3 E3 E6 L }
7 H* N& m. W, x9 e: J+ ~1 A}2 A n- F! H( M9 |1 ^- s" I4 Z* y0 z
来个spring配置类3 @2 R2 S/ z% s/ T, U' T) l
1 I2 p6 v) ]- s9 Q5 r& ?& D+ Wpackage com.javacode2018.lesson002.demo18.test2;3 F9 |. S" A2 Y4 i+ N
- B( {2 P' v) _9 I
import org.springframework.context.annotation.ComponentScan;
x; |, E- M9 ~import org.springframework.context.annotation.Configuration;
) a7 h/ s: @0 l. \
- R' `! |3 k8 J7 g9 l5 E@Configuration$ V! c/ W5 U8 n
@ComponentScan
% f% L" P. h- Z4 Jpublic class MainConfig2 {
( y5 ]) c. A! T1 S1 V}
6 C2 C$ V8 {2 ?0 Y/ m$ U下面是重点代码5 @3 S5 F0 K, c& v& [7 A
( m1 h# J7 n2 r1 ?) h@Test; G" \/ a4 \% t! H2 P
public void test2() {
8 s {8 a$ z- i5 m( r AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
2 ] N1 E( `# @, U1 O, b+ g1 Z$ x3 U, [! j/ ~. r: S
/*下面这段是关键 start*/
# z0 ^ r$ J6 K! W //模拟从db中获取配置信息
/ n" ~/ _5 k7 l5 g( [8 V6 f0 J" E( I" P Map<String, Object> mailInfoFromDb = DbUtil.getMailInfoFromDb();- @7 X% Q- H0 B' s2 i' H o+ h
//将其丢在MapPropertySource中(MapPropertySource类是spring提供的一个类,是PropertySource的子类)# Z/ G6 I! n: k9 b N/ u- i
MapPropertySource mailPropertySource = new MapPropertySource("mail", mailInfoFromDb);2 B# ^) I; s. p# L6 M9 R
//将mailPropertySource丢在Environment中的PropertySource列表的第一个中,让优先级最高
' ^) ~: t$ b' } A0 _8 R context.getEnvironment().getPropertySources().addFirst(mailPropertySource);
; L2 q: ^ F( n# C3 p /*上面这段是关键 end*/
0 W. G: Q: H) f8 E6 F
9 Q2 e5 H3 c+ j context.register(MainConfig2.class);# B2 q, O1 P0 w7 m% Y( ^0 K1 j
context.refresh();$ e" z0 U5 `# B% k" i
MailConfig mailConfig = context.getBean(MailConfig.class);
8 V+ k2 O u) `/ z2 i- {7 b9 k System.out.println(mailConfig);5 d- M0 @6 ]& b1 m- u! _' N5 s
}: D& {* `# T4 N! C/ k% P
注释比较详细,就不详细解释了。
1 G5 B& C- X( W/ h3 w: o' ~, U7 X% \1 ], ]" n$ r. k9 j3 L: H
直接运行,看效果
3 r. W0 m" R* x$ E% `0 w! V
4 {0 v6 H* D( _# R# H3 [6 gMailConfig{host='smtp.qq.com', username='路人', password='123'}
2 U8 D1 s( F4 z- p; v有没有感觉很爽,此时你们可以随意修改DbUtil.getMailInfoFromDb,具体数据是从db中来,来时从redis或者其他介质中来,任由大家发挥。
: ^% _2 T) u# e% C0 x
: [. I% B6 d# d1 H4 L: O' R上面重点是下面这段代码,大家需要理解/ ?* \0 \5 H8 L Z7 r# U
3 @ A, C9 p6 O" G& h9 i/*下面这段是关键 start*/* M" ~7 j9 @, D' X) ^
//模拟从db中获取配置信息
, E" G6 Q% Y" c0 k; V/ ?3 |Map<String, Object> mailInfoFromDb = DbUtil.getMailInfoFromDb();, K4 k3 x' H( |, `- a
//将其丢在MapPropertySource中(MapPropertySource类是spring提供的一个类,是PropertySource的子类)8 L4 ~/ {7 X4 z' E* @9 T
MapPropertySource mailPropertySource = new MapPropertySource("mail", mailInfoFromDb);
# }: a* n) R+ H3 U2 z& \: c//将mailPropertySource丢在Environment中的PropertySource列表的第一个中,让优先级最高# g9 x8 v! V# \# L2 D* u( L; |
context.getEnvironment().getPropertySources().addFirst(mailPropertySource);
8 s6 c! t0 d8 Z j: f5 v1 h) O7 U/*上面这段是关键 end*/2 ]' S6 ~8 n% H! G }. m5 M
咱们继续看下一个问题" ^9 _. i) a w! _
% [$ N$ e7 `, o* X
如果我们将配置信息放在db中,可能我们会通过一个界面来修改这些配置信息,然后保存之后,希望系统在不重启的情况下,让这些值在spring容器中立即生效。! G6 G! X; ^: [1 ^
3 W9 V+ C+ t" @7 p% J. V$ w0 `@Value动态刷新的问题的问题,springboot中使用@RefreshScope实现了。5 C% P, R( U! @6 i# O
, T1 n0 j$ \! U! p, m5 p; \
实现@Value动态刷新/ p- ], U9 v ?; d% Q7 E- ]
* Y6 o6 T0 L* L! s+ x4 Z/ h$ m先了解一个知识点5 F0 x9 s1 s! {8 U3 O+ m' C
# \) n& |5 U$ m: u3 r2 z这块需要先讲一个知识点,用到的不是太多,所以很多人估计不太了解,但是非常重要的一个点,我们来看一下。
- h$ q7 o$ z/ H5 L: E
( j1 O9 |/ W3 }$ I* M* G: w; _这个知识点是自定义bean作用域,对这块不了解的先看一下这篇文章:bean作用域详解
1 a& ]3 _, A3 ~1 x7 m- _6 x) I% s- q( W3 L6 }" X4 k& P
bean作用域中有个地方没有讲,来看一下@Scope这个注解的源码,有个参数是:! k I, S5 S8 m$ A2 R5 z
9 r9 N1 H3 K5 F; I4 `& h( yScopedProxyMode proxyMode() default ScopedProxyMode.DEFAULT;
1 v6 U" r6 n' p! w* y( r这个参数的值是个ScopedProxyMode类型的枚举,值有下面4中4 }; T8 I2 d$ q# o; f
$ f. z& s r8 K% W- vpublic enum ScopedProxyMode {
+ u4 Q) }' m, H4 Y2 Q DEFAULT,
. A" L1 X" f) s& }4 z0 ~ NO,5 ]2 K3 O/ _- x% X4 `0 `+ D
INTERFACES,, s1 H: Q& D, r
TARGET_CLASS;% L8 n6 }# ]# [# u. Q5 }
}
- P& L- h8 z- ?前面3个,不讲了,直接讲最后一个值是干什么的。
# c& r$ p7 I2 o) Y; u9 N2 x, A: m- A0 F. p! q
当@Scope中proxyMode为TARGET_CLASS的时候,会给当前创建的bean通过cglib生成一个代理对象,通过这个代理对象来访问目标bean对象。
/ x6 h \" l; p
6 J" q2 }6 H. e. N% G+ U! w理解起来比较晦涩,还是来看代码吧,容易理解一些,来个自定义的Scope案例。
/ h; T8 B7 W* i4 w* H' o7 C3 p- b" [/ j) v$ Z) o2 D% Z
自定义一个bean作用域的注解
, {6 D, Y5 f) @. |5 ]) a Z
# y2 ^& O& K- s3 Vpackage com.javacode2018.lesson002.demo18.test3;# [. m) t P: A4 r* V6 T, H- J
1 C: }6 z7 H3 A& i; B1 Dimport org.springframework.context.annotation.Scope;
! l) ~; f f+ X5 u( f% zimport org.springframework.context.annotation.ScopedProxyMode;, G8 A/ I+ m7 A1 `" d V' P4 l0 o
+ d! `2 u! E4 v3 P
import java.lang.annotation.*;
3 l8 @( ~; Z; h$ Z' e# d6 c) i" G" m e0 y0 g9 d
@Target({ElementType.TYPE, ElementType.METHOD})
0 t2 t3 n' f, ]# E@Retention(RetentionPolicy.RUNTIME)
6 n6 y& P+ \, ^8 @- b@Documented
: y- r w6 b9 Q@Scope(BeanMyScope.SCOPE_MY) //@1
" B' o( F4 e3 s) D- s; g9 _public @interface MyScope {$ k8 m" e8 Z; ~. I" _
/**
3 [# U2 o+ x7 \7 j * @see Scope#proxyMode()
0 N% G6 P f: c( H% A8 y */
6 m6 K3 h' b( ] { ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;//@2! ~7 }. Y' s& d4 z p! f
}" A9 X2 g7 L- l9 E2 L/ E* }
@1:使用了@Scope注解,value为引用了一个常量,值为my,一会下面可以看到。
0 e- }! ?( {8 C- U8 K( \9 ]) ^, P% b3 S' E% n
@2:注意这个地方,参数名称也是proxyMode,类型也是ScopedProxyMode,而@Scope注解中有个和这个同样类型的参数,spring容器解析的时候,会将这个参数的值赋给@MyScope注解上面的@Scope注解的proxyMode参数,所以此处我们设置proxyMode值,最后的效果就是直接改变了@Scope中proxyMode参数的值。此处默认值取的是ScopedProxyMode.TARGET_CLASS
% ]! B F+ w- S/ ^9 U) B5 E0 a3 \3 T% W7 q; x# o
@MyScope注解对应的Scope实现如下
5 _7 Z4 W) H* V: i: X0 ] V( ?) \& X1 r1 X
package com.javacode2018.lesson002.demo18.test3;
# U9 z* @" u. v- e; M7 T
+ L5 T9 g: i9 r! L( S' P3 N! Kimport org.springframework.beans.factory.ObjectFactory;
# M- T4 ?' G7 h( F1 Limport org.springframework.beans.factory.config.Scope;2 Y8 J% X! i* S+ J
import org.springframework.lang.Nullable;
% l7 _: N) K& g$ i1 q! R2 ?! i8 s: |" _/ ^! S0 i
/**3 o3 s9 O) J' r
* @see MyScope 作用域的实现
5 b- m$ L) h @0 S */; k+ Y" r {( E; |' t o
public class BeanMyScope implements Scope {7 G: `8 j* Z7 L) U
; q8 ], L2 C! r' E) F7 C5 P& ^# e+ v
public static final String SCOPE_MY = "my"; //@1/ m B7 F" x; s* B
& O; P7 a' v9 r @Override6 ]9 i/ d1 v5 @/ c c4 S
public Object get(String name, ObjectFactory<?> objectFactory) {
% G6 }5 ^8 N; F' V J; Q System.out.println("BeanMyScope >>>>>>>>> get:" + name); //@2# f: Y+ a; \8 H6 J- ^" j" J
return objectFactory.getObject(); //@3
, V; T; c3 U) t8 H }
- H' I5 s6 ]5 q( N$ p( o
1 ~6 S! R: W2 o @Nullable
( O* e2 n: R5 x, D- R# t2 P @Override
' r; K1 T; y2 H: Y& T public Object remove(String name) {. t# m% w) P, w- W" ]4 G
return null;
! S& O8 K" k% D }; Z# P, l* f7 L' O( n
* l9 S- g3 F+ f6 V' L* p8 j
@Override! E4 G7 e }* u
public void registerDestructionCallback(String name, Runnable callback) {2 q# P- q/ W; Z; S. f
6 n$ _# U/ j( z" P6 O4 n }2 c5 {4 v/ W. m# m
1 D! W" r8 i* k
@Nullable8 ?/ f( o; ~! u+ M3 p3 h2 P% J j% ?
@Override5 B2 _ |2 V/ J
public Object resolveContextualObject(String key) {
" U" W; u& F2 T+ d return null;
" a. B2 @& s2 Y) d, O }
" D8 F& i: W" y. X) S$ P9 w- J' Q( s! ?; F; g' n8 l1 }
@Nullable
8 I7 n. G% H- o6 Q3 p @Override4 \6 a# G' E, w4 w- t
public String getConversationId() {( j7 n% s; y! O2 D% }3 Y$ ~" D
return null;
, R- _0 D6 b! {" } }+ q# O* |! X4 e
}, Y; W6 u1 D8 n7 k0 v* D
@1:定义了一个常量,作为作用域的值7 l" I j+ h) J% y/ q6 L3 R, m, ]
: Q* t- l2 N: d9 u3 U! Q# w+ B@2:这个get方法是关键,自定义作用域会自动调用这个get方法来创建bean对象,这个地方输出了一行日志,为了一会方便看效果
, b$ S; p* {3 _1 ^) L$ a3 Q
0 M1 N$ M- Z$ y# a/ I j$ ^; ^@3:通过objectFactory.getObject()获取bean实例返回。
, f8 d1 f/ ~& J# S. n
0 n4 F: u0 ]) |* C$ R8 u9 k+ ^下面来创建个类,作用域为上面自定义的作用域
' {9 \: N5 b. s
4 m7 @$ U) y! r, j$ ^ Epackage com.javacode2018.lesson002.demo18.test3;4 S. @6 C g! }) d9 n# p
' K6 V) A; ~( G D5 }% g/ }import org.springframework.stereotype.Component;2 U+ U- c2 O& |+ b7 b
$ T$ N, L6 W! _6 k' z: t. k! Timport java.util.UUID;% G3 c$ a' U! [
: G. t7 m) c1 }@Component
" ~$ E+ S7 w( [2 L6 `; n6 K8 g0 A@MyScope //@1
7 Q- v* u0 i! p0 `. c0 G. Y6 dpublic class User {
! [0 `" d, R9 D# I, {
$ X1 h+ V) ^/ u# Q0 c private String username;
( C0 h. O6 X) C% a; p
- I6 v8 Y% |6 r% a% r public User() {
4 W! E# H* i8 c9 z, v System.out.println("---------创建User对象" + this); //@2
8 C! z, {1 F$ c& Q- u$ P this.username = UUID.randomUUID().toString(); //@3
2 R3 p; e9 H/ n( s$ G7 u7 I }( f' N/ V6 ~8 O7 e% M( ?
+ K+ t& {* Y3 Q9 z public String getUsername() {' j/ L( J' M; H9 V1 t
return username;
1 s7 E1 k0 v6 C2 ` }
/ e5 w$ \0 ]3 M: f8 {' ?+ ?5 V8 K5 Y' I# Z# H
public void setUsername(String username) {
) |* D5 l; H% e) r2 T2 e, ^ this.username = username;
+ Q/ ? N7 l. l) \0 r9 m- | }3 b) M# i" u# X1 j3 }2 V% c
9 s: G& k) I" r W, c. n$ F
}$ A! M8 D# s5 s3 i
@1:使用了自定义的作用域@MyScope
3 z! G, K$ G! S6 J/ x7 A* v$ }1 G8 J4 D
@2:构造函数中输出一行日志; C! K% J0 [0 |2 Z
( N) W/ I( d9 J% p@3:给username赋值,通过uuid随机生成了一个 S# g3 P! V. G+ ]$ F! w1 G
. t/ h5 g: B+ o4 N4 u1 D1 w
来个spring配置类,加载上面@Compontent标注的组件9 @, Y7 Z7 k2 i# }6 D
0 B# p4 {8 I' [6 g, Ypackage com.javacode2018.lesson002.demo18.test3;7 f. ~$ w z' h ?1 O
, O+ `5 z9 W. N( } a
import org.springframework.context.annotation.ComponentScan;
/ L7 Y+ F: \: p" a% vimport org.springframework.context.annotation.Configuration;
) |( {7 Y# z. W, Z0 G5 ?: A, \
2 r" \& h9 A: T, A# a@ComponentScan
' y u1 x+ h* X$ m" C@Configuration8 k, T6 D s; k2 c; I6 E
public class MainConfig3 {
' l c8 |$ }5 f+ W( q- J; N% p, ^}( b Z% q* f# ^
下面重点来了,测试用例5 O6 ^$ g4 C9 n% Q
. Y" O- K: f7 ~' V@Test$ F; Y1 P( o1 ^) m& T0 R( N q
public void test3() throws InterruptedException {
; F' v9 O( H4 r% t! q AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
( |6 b9 q* i5 h4 u //将自定义作用域注册到spring容器中8 k4 @' q- K+ {: i+ Q" B1 s
context.getBeanFactory().registerScope(BeanMyScope.SCOPE_MY, new BeanMyScope());//@1. B& p% M: P( Z9 |+ d' z
context.register(MainConfig3.class);5 t% @) V0 n2 }& t# k$ _
context.refresh();
) p7 o3 L# |) U' [
9 C- Z/ v8 t |. { System.out.println("从容器中获取User对象");+ R2 c% x; R3 n& Q; N9 } ?' _
User user = context.getBean(User.class); //@27 r! I* w+ Y- n& Z* F# H; G" T
System.out.println("user对象的class为:" + user.getClass()); //@3
3 i# K% A9 A2 G; N2 K5 V$ j
Q' B- q8 v0 G& F8 v ? System.out.println("多次调用user的getUsername感受一下效果\n");. {+ ^2 {# i/ G+ ~3 U) C( k. M) L
for (int i = 1; i <= 3; i++) {
# b; @! H( z' x; D+ U1 ^ System.out.println(String.format("********\n第%d次开始调用getUsername", i));
; } d6 P7 d2 k; j6 Z: } System.out.println(user.getUsername());
" ?3 V5 B9 T P4 M' f% {; X9 q System.out.println(String.format("第%d次调用getUsername结束\n********\n", i));
2 d9 t: R+ t/ l8 R( A- y. M }% Y; Z" B% j% s+ K
}- o, A3 A( J! |
@1:将自定义作用域注册到spring容器中1 a1 ^5 O. _; G8 ?3 f# q- l
- U5 y; d$ B$ V" m- ?& }8 W
@2:从容器中获取User对应的bean0 T- g3 ?6 W! v" N" o3 [
: g. m! Y5 w( ~" F6 |
@3:输出这个bean对应的class,一会认真看一下,这个类型是不是User类型的' q9 ^( _4 G% `# e
5 E/ |! y# i3 r( Q4 ^% k代码后面又搞了3次循环,调用user的getUsername方法,并且方法前后分别输出了一行日志。8 K4 `4 ?" @ p. [8 {8 J
: Y$ k1 `# n- T: V w1 a( r9 N见证奇迹的时候到了,运行输出
9 f- k4 Z: z2 @: M: ?3 |) Q
0 L1 k2 b) \5 ` n1 m3 K" L8 L从容器中获取User对象
z) `+ \7 O' G5 Puser对象的class为:class com.javacode2018.lesson002.demo18.test3.User$$EnhancerBySpringCGLIB$$80233127& i1 A9 N# A6 f# R; }0 r8 u% q
多次调用user的getUsername感受一下效果- i' h6 z5 i9 S1 [; U& G) n
! {; k% c. E/ y7 K }
********2 `' j0 G: V t, s, x
第1次开始调用getUsername2 s5 c/ I' E& X; R
BeanMyScope >>>>>>>>> get:scopedTarget.user* P3 l$ f) A K# I
---------创建User对象com.javacode2018.lesson002.demo18.test3.User@6a370f4
: W6 f, b" B" C( Z% Y5 C7b41aa80-7569-4072-9d40-ec9bfb92f438
3 `5 a+ d* \! e( I第1次调用getUsername结束
9 ? v' m& |& r$ n9 O5 ]' W# D* u********
$ p( @/ w; O9 \4 T
! |& f% i C x. E- k********
9 y1 N7 F, j- O; L0 B第2次开始调用getUsername6 t* u0 ~3 J4 F: n1 M* r/ |
BeanMyScope >>>>>>>>> get:scopedTarget.user
* L' v; Z1 f. f* v' _ R, r# p---------创建User对象com.javacode2018.lesson002.demo18.test3.User@1613674b4 y( ~2 R# `8 s
01d67154-95f6-44bb-93ab-05a34abdf51f
# L" H6 E9 z; |8 ~第2次调用getUsername结束$ B& f- w8 o* c# q7 Y
********
T( i& [5 ?9 `5 ~* x N, _1 S" p& l: s. h$ X
********
' [& M( r( b1 m! T, u# s第3次开始调用getUsername, L4 e, k. M* L) U& ^3 Y
BeanMyScope >>>>>>>>> get:scopedTarget.user$ q- [$ `! {" w$ t* q2 x/ ]* ?- @" S
---------创建User对象com.javacode2018.lesson002.demo18.test3.User@27ff5d15
% w1 U* F) X4 @8 }8 r76d0e86f-8331-4303-aac7-4acce0b258b8
5 B/ n0 M( Q9 w* _ a第3次调用getUsername结束
2 {9 ~! M! w- t& V4 x! E, {********2 }& f4 S; t8 t5 Q' w
从输出的前2行可以看出:
, ]2 T. ^( R' X4 m, k7 b9 Z2 t% M' }- }0 L1 n, Z3 ?6 Y: N
调用context.getBean(User.class)从容器中获取bean的时候,此时并没有调用User的构造函数去创建User对象
( J0 H" [+ N( X, i3 Y. n# Y: S: _, P2 U0 _ a
第二行输出的类型可以看出,getBean返回的user对象是一个cglib代理对象。, g# S3 `9 Y8 M( n% k
+ _! T& O& I( Q! _2 @后面的日志输出可以看出,每次调用user.getUsername方法的时候,内部自动调用了BeanMyScope#get 方法和 User的构造函数。/ O3 {% S- G9 O) G$ Z1 ^7 a6 O
( c8 \ ]% l+ _2 \3 Q Z9 l% T通过上面的案例可以看出,当自定义的Scope中proxyMode=ScopedProxyMode.TARGET_CLASS的时候,会给这个bean创建一个代理对象,调用代理对象的任何方法,都会调用这个自定义的作用域实现类(上面的BeanMyScope)中get方法来重新来获取这个bean对象。: A& G' _& v+ n3 b9 ~
$ g* ^! D7 Z& X4 X2 L/ Z. h动态刷新@Value具体实现% _1 c# Y1 o; k1 }
# M, Q/ }+ [0 A' t
那么我们可以利用上面讲解的这种特性来实现@Value的动态刷新,可以实现一个自定义的Scope,这个自定义的Scope支持@Value注解自动刷新,需要使用@Value注解自动刷新的类上面可以标注这个自定义的注解,当配置修改的时候,调用这些bean的任意方法的时候,就让spring重启初始化一下这个bean,这个思路就可以实现了,下面我们来写代码。
3 P: R# l+ @, A3 [) E U `" n' O4 y7 p' U2 p# i1 a% ~
先来自定义一个Scope:RefreshScope
5 A4 M$ v0 W; d* c) B; G) K; S
8 U" I# U6 `1 L0 f" @package com.javacode2018.lesson002.demo18.test4;
+ J# D, d# e) B* z3 r
9 U) p2 p% I% gimport org.springframework.context.annotation.Scope;8 y- S5 O4 Z3 r
import org.springframework.context.annotation.ScopedProxyMode;
. x4 U6 s( P) M7 @
& {/ [" A) `" @2 i; }import java.lang.annotation.*;: V4 G2 @' y4 Q+ j- g+ \% {+ K
2 @9 p$ V" J( X& W1 g U' v* k
@Target({ElementType.TYPE, ElementType.METHOD})" |8 _* R u: n. `
@Retention(RetentionPolicy.RUNTIME)
! i) b/ S9 t7 _& X9 E" h9 c@Scope(BeanRefreshScope.SCOPE_REFRESH)5 R) [2 D& x$ S5 j% N
@Documented
8 P7 `& e9 x$ Q4 r% Npublic @interface RefreshScope {
/ j5 K% P! u9 U5 ]" _ ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS; //@18 P$ w7 A. i b+ E& _: O
} O0 q, }: E" q X
要求标注@RefreshScope注解的类支持动态刷新@Value的配置5 B* u3 X: U; c4 o9 t
# S5 F. i- c, g2 e( \
@1:这个地方是个关键,使用的是ScopedProxyMode.TARGET_CLASS
1 |* G5 a& q- U6 d2 Q/ A, _6 @8 f; L; [; M1 h$ v$ d' _7 N. [
这个自定义Scope对应的解析类& D6 w. |, k; V
+ H6 `+ w+ E! ~" P I! _# H' t- C# ?下面类中有几个无关的方法去掉了,可以忽略1 K* I8 @# f+ X$ Q
3 L7 J) Q( o% E6 R' Vpackage com.javacode2018.lesson002.demo18.test4;
& h( s: b5 h( C. \/ W
3 Y% b2 D3 K' x3 v9 _
- P3 k6 ~3 ?7 ^* R/ E+ Y. [import org.springframework.beans.factory.ObjectFactory;
! X$ d: k" e0 Fimport org.springframework.beans.factory.config.Scope; \* Q7 U8 l# N: I7 X- F& O4 ~
import org.springframework.lang.Nullable;
- e. ~2 W3 {7 T0 q5 `8 E; v1 a9 V! V* |( o& z) `& y) s
import java.util.concurrent.ConcurrentHashMap;& f/ T) i2 Z. x W1 e" }
' E8 U: N6 x \, j! [+ Q
public class BeanRefreshScope implements Scope {/ E, I: _, M& D1 `9 E5 e! a! G# ?* k
& F6 H7 Z& N4 f! W. p8 V- j
public static final String SCOPE_REFRESH = "refresh";) j0 Z b: k7 R* A. K! F' B0 \
1 E" {& s1 e" ]8 ]6 R& J3 g private static final BeanRefreshScope INSTANCE = new BeanRefreshScope();
& x( p- i% H5 j1 v( E( A
- r/ l J1 A/ q, d //来个map用来缓存bean
. f8 @; _! x( \5 q& C0 { private ConcurrentHashMap<String, Object> beanMap = new ConcurrentHashMap<>(); //@1% C7 Y9 ?; ?4 e( J" c, [, [
; U7 G j7 R- a8 J" R0 t) ]/ j private BeanRefreshScope() {
& G( r- Z- a, I0 s$ N }
1 G/ a3 Z, S& z* W: E- g7 P' \" V" P
public static BeanRefreshScope getInstance() {
, h2 `1 f- K7 a5 p return INSTANCE;
0 R9 E$ L4 ^' i }
1 r! Z% N b/ z, O' F9 U9 h
2 y: k" G6 u/ `5 D* ?( C( C /**
5 h `6 f- C+ Y% j * 清理当前
' c5 p5 U: R6 l$ s: f */2 \' i. f$ S" ?* q p8 @# V. T: d2 T
public static void clean() {" a* a8 r* V6 ]2 w& Y* R8 _
INSTANCE.beanMap.clear();" e% u- Y( v0 K: f% @
}' _# V( P# ]4 `. G9 U# H2 a- x
# L8 f- h, t5 p& c ? @Override5 K2 n! A: p9 P
public Object get(String name, ObjectFactory<?> objectFactory) {+ S6 y$ Q* L' v" E! V7 u
Object bean = beanMap.get(name);
3 n' k( j$ l2 B if (bean == null) {
2 ^' r4 l& A4 I bean = objectFactory.getObject();
/ |) U. f7 J. p% _6 T beanMap.put(name, bean);
: ^ X, |) G% u" n) i9 z& S. M }
! p8 \" g; ?. {( j9 @0 z6 {) f return bean;; Z7 s' W+ V% `, J' P- I2 r- `
}
0 C8 d8 @; O& l+ P6 E/ R1 r! ~( i4 D) _
}, E6 p t1 P! n. M8 I/ q
上面的get方法会先从beanMap中获取,获取不到会调用objectFactory的getObject让spring创建bean的实例,然后丢到beanMap中
$ L: L9 q$ ^4 T' R' O* ~* t8 c2 }3 L) Q, M5 ~+ c
上面的clean方法用来清理beanMap中当前已缓存的所有bean
* ]9 H+ Y. l2 y& J7 z" C# n6 r# @3 Q: P/ ~$ S+ E7 Y
来个邮件配置类,使用@Value注解注入配置,这个bean作用域为自定义的@RefreshScope' i$ t7 B; q" `1 I0 g i
$ k0 x0 b3 b0 k! L9 X
package com.javacode2018.lesson002.demo18.test4;
7 v v# O( s G6 l. ]! B/ P
2 g' M/ h% I6 E Vimport org.springframework.beans.factory.annotation.Value;& ^; j" H& M7 F% `' k9 y" G
import org.springframework.stereotype.Component;7 e2 s: K9 a5 S6 m; l+ j, f% b
3 b8 `% }/ T$ m5 @( w& d5 K
/**; x5 G& H! |! q2 K) `0 h8 V
* 邮件配置信息 o. Z2 |6 D& Z+ t% N
*/4 [" ~7 _* r5 f% k7 {3 f) L
@Component
1 z+ z- ~6 p) ~7 v9 s@RefreshScope //@1* {0 T0 z# ?5 [- f* C1 T9 x
public class MailConfig {
+ f0 _! H9 l+ e/ }$ R
5 G* [' t& y3 M* R& u1 X. G, p8 ? @Value("${mail.username}") //@2
/ c: l( ]8 I! \1 A& R4 `9 S private String username;
) i8 f7 Y" o$ N! r% S$ s0 R5 S" f Y# i( Y. j; `
public String getUsername() {7 u/ e. h/ u; U$ p
return username;
- _: l& s, c' i/ A+ N; _$ Y& a2 i }
/ m5 i: c- `( l7 }7 O8 X/ t3 Z/ U& ^! J5 I
public void setUsername(String username) {
. {3 J, n9 P8 P* x this.username = username;$ ~+ z! g$ @. r8 L7 e i8 }
}6 L6 F0 d/ N0 t, @
% i& s' x: o+ n$ y @Override
/ X* X; q) m& Q1 K- ^4 i7 k2 L public String toString() {
$ H! j4 P/ d% {6 a) R: M6 ` return "MailConfig{" +
( ]# y8 Q0 D' p2 k$ t, r o "username='" + username + '\'' +6 o/ a, j) k8 f3 l. ]
'}';! p) c( f! k! ^
}, [) |- U% b1 i) p
}
/ z3 a( T+ A5 }6 J2 l@1:使用了自定义的作用域@RefreshScope
2 u1 ~, p& f$ G( Q* f& ?) B. H8 N' k2 A' Q
@2:通过@Value注入mail.username对一个的值7 O& l' q8 i* u/ a
, G4 v2 N$ A' w重写了toString方法,一会测试时候可以看效果。; j+ S$ R( F0 S( m$ B4 k$ _
; _" M* A, {# J) p+ L* ]* s$ ~
再来个普通的bean,内部会注入MailConfig
+ Y0 G% Z o U: ~% y- @1 Z/ E7 g* ~8 T, z- }% q
package com.javacode2018.lesson002.demo18.test4;
: k P) p8 J# W$ ^: y8 m# ?! @, l$ p! N. E) K) u7 L
import org.springframework.beans.factory.annotation.Autowired;
# L2 w% P2 J0 p) k0 O' \import org.springframework.stereotype.Component;1 _. T1 g. F8 x1 t- _# T
6 @0 }$ ?; q0 E* w* j$ r, y
@Component0 M- \9 M3 n& M' O
public class MailService {
0 B) U8 ^2 r- R1 j. H @Autowired4 L6 L7 D& ^% j& Y, c9 ]
private MailConfig mailConfig; v* @# X [2 e2 q0 M6 a
. v! z7 w) Z; }" k, o! U# m @Override
; l8 V) q6 L( C8 g public String toString() {
) _" s i4 |- b( P6 z return "MailService{" +( O. e' A8 S) ~. k
"mailConfig=" + mailConfig +) g6 Q5 `+ B) `4 `! y) |7 O
'}';
6 d8 k; }+ i0 x& ?; d }2 W! f3 u$ j5 v- I7 \3 M: b
}
9 ~- G) _4 y; n% P3 a+ V3 _代码比较简单,重写了toString方法,一会测试时候可以看效果。
% O! _' y2 j$ \+ O& }! \
8 f/ Q' l; T( @/ C- R0 T d来个类,用来从db中获取邮件配置信息' G [! G2 _9 X* }7 s! N
" `+ d6 g+ ] t
package com.javacode2018.lesson002.demo18.test4;# M2 j: d+ [) g+ R( s" O1 R, q) w- M
% W! O: r7 T5 ] yimport java.util.HashMap;1 A! }) s; O0 w; p3 c3 j; j: f: f
import java.util.Map;, @* Z* g* m5 y9 g. ~) y. g
import java.util.UUID;
5 r$ j8 M( `! j. g v! ~3 q
3 Y9 }: p# |6 ^public class DbUtil {
% ?# J H# n' \( V /**
" c/ V) E4 ~! `0 F3 N" N * 模拟从db中获取邮件配置信息) R/ Z' G7 I! q" Q; k9 H- l: ]
*' ^. c ~6 h' G3 n; x
* @return- A( N i& S7 p& F% `" e- X
*/2 k* Z7 |: f) c1 A; [- Y. x" j
public static Map<String, Object> getMailInfoFromDb() {
6 _3 `. a" ?; W F. N+ s- N Map<String, Object> result = new HashMap<>();% q* L; Y; Q( I
result.put("mail.username", UUID.randomUUID().toString());
$ O; s. `$ T5 u/ n6 R return result;$ D$ M0 B- w K0 }( ]9 j# A6 G6 _4 u8 t" y
}; d/ B: _+ E3 N
}
1 b& H" \2 } U1 Q来个spring配置类,扫描加载上面的组件
% P: h! d/ G7 d8 X V
1 L& ~; Z4 H: E% `0 Y; c9 Apackage com.javacode2018.lesson002.demo18.test4;; D$ r( D, n% Y) A5 k9 n
1 x1 h' W |; s: F1 k9 g" U1 f, Uimport org.springframework.context.annotation.ComponentScan;" \. _& z% \* v! J/ `
import org.springframework.context.annotation.Configuration;
$ M* Q- F' K4 Z5 H2 e: L% L+ e4 w7 A
* q8 l, C: B0 \7 H) I4 _@Configuration
$ [0 s ~, Q1 G: Z$ [7 E@ComponentScan
4 E0 S, v, M! z& n- wpublic class MainConfig4 {' U. u% }' n; M
}
, t/ ]3 b: N& U3 p& c, x/ r来个工具类
/ b( n: }0 S9 t% h/ ]
) F2 I9 }* ~1 r7 |内部有2个方法,如下:
7 ~/ S: g1 M6 O. ^% I9 A
' j8 B( j0 o& c4 |package com.javacode2018.lesson002.demo18.test4;
+ i2 T/ I1 Q+ x- |# L: c) [ j" z' e/ H2 f
import org.springframework.context.support.AbstractApplicationContext;
( X! i$ D! o& v# r% S; Pimport org.springframework.core.env.MapPropertySource;6 a, K; ]9 {7 U9 G! S
1 T4 u u! U# ^6 w' [) P# {3 `$ Nimport java.util.Map;
( w- `) y/ _7 t8 l7 o" l' r$ b/ Z0 A8 d
public class RefreshConfigUtil {
; F' N& s. |! V8 P' K( R /**
* `/ B4 \. h1 V- u% N. p0 H * 模拟改变数据库中都配置信息+ s" k" N# u# \) r, X% p" O" @8 f9 o
*/
2 L& s8 Z3 {- [% u7 f) } public static void updateDbConfig(AbstractApplicationContext context) {
- C& P" q+ h# q& |* d% p% Y" _ //更新context中的mailPropertySource配置信息
( h' o7 z" Q; z, n2 y& e" T) J refreshMailPropertySource(context);
1 n7 p6 I/ d# e& E8 _; C: {: e8 W1 o: T0 ]- T M
//清空BeanRefreshScope中所有bean的缓存
' r% P6 C( O# R6 B% q BeanRefreshScope.getInstance().clean();
& p) f, H7 G0 s2 Z }3 I- N1 n+ [9 F0 R
& C. J( ~1 y' \+ ^* ]; `- C' n! t) {
public static void refreshMailPropertySource(AbstractApplicationContext context) {; h$ [. o2 x5 t' O6 U8 G5 t
Map<String, Object> mailInfoFromDb = DbUtil.getMailInfoFromDb();2 E( ^3 q4 s: Q+ a* q
//将其丢在MapPropertySource中(MapPropertySource类是spring提供的一个类,是PropertySource的子类)" }( j D2 m' ?' K T
MapPropertySource mailPropertySource = new MapPropertySource("mail", mailInfoFromDb);
5 }. H n8 B& n* n |3 | context.getEnvironment().getPropertySources().addFirst(mailPropertySource);" g( S5 t0 \/ o& T4 t
}
* f1 ~8 B# k8 Q$ z0 P3 P5 v4 r- r: N
; F w2 X1 n! T' k7 [}; I$ P) E, `% e: A) j
updateDbConfig方法模拟修改db中配置的时候需要调用的方法,方法中2行代码,第一行代码调用refreshMailPropertySource方法修改容器中邮件的配置信息
5 T M# P; g8 ^: n; L' w& W* r1 A7 W# l
BeanRefreshScope.getInstance().clean()用来清除BeanRefreshScope中所有已经缓存的bean,那么调用bean的任意方法的时候,会重新出发spring容器来创建bean,spring容器重新创建bean的时候,会重新解析@Value的信息,此时容器中的邮件配置信息是新的,所以@Value注入的信息也是新的。
; c- g8 Y; Y' _; p# k. j9 C! \- G6 D6 J
来个测试用例
! G6 B# e! v0 X0 V! @9 w
8 k2 x' ?6 L% i: ^& e@Test- g. }: M9 Y9 \5 V' _' @. N# I
public void test4() throws InterruptedException {0 I6 i( |+ C; H1 n
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();' e) L4 C: J+ L* H8 |9 k
context.getBeanFactory().registerScope(BeanRefreshScope.SCOPE_REFRESH, BeanRefreshScope.getInstance());
( e) {3 ^- H: ^ q3 \. R context.register(MainConfig4.class);
. W6 E7 a! w& m3 Q( q3 o //刷新mail的配置到Environment$ `- D% s7 n$ e, K
RefreshConfigUtil.refreshMailPropertySource(context);% c `9 a( R5 y7 m) l+ ?
context.refresh();+ h) y7 k4 E# Y7 }; g) C: f
; i+ h3 H3 ?( b9 J0 S MailService mailService = context.getBean(MailService.class);7 v* W; t) t* d. G
System.out.println("配置未更新的情况下,输出3次");& F) x. I3 e M" k$ f, \0 A' v
for (int i = 0; i < 3; i++) { //@1 y8 p3 Q2 V9 {4 D$ ?) g1 `) v, }
System.out.println(mailService);: a4 a% ]: U; B% |1 r$ I
TimeUnit.MILLISECONDS.sleep(200);: M& I! N) t- ]8 N& }" q5 |. g' z
}
/ m( g, d$ k8 V: B# c1 [; N8 s! n( _9 }3 J$ ^7 @& }% G0 w
System.out.println("模拟3次更新配置效果");
" G4 i, \% M) j for (int i = 0; i < 3; i++) { //@2
: t/ }; {" V- _& O; P! s RefreshConfigUtil.updateDbConfig(context); //@3' u& l8 ]& c" t' _
System.out.println(mailService);
( T) h) Z) \! R9 q6 Q: i' t TimeUnit.MILLISECONDS.sleep(200); ?9 t; r& |4 k# | c
} |7 G& v1 q$ ]
}7 V8 x a1 E2 `& V. h$ ?6 m1 T) M
@1:循环3次,输出mailService的信息# B+ D7 V; p5 G7 I
3 _) J+ L# y! J3 _@2:循环3次,内部先通过@3来模拟更新db中配置信息,然后在输出mailService信息
+ x" g- c3 n0 @3 {4 _& p
2 v; B; J6 l, p- p9 Z见证奇迹的时刻,来看效果4 S2 K- m( p! [6 M. _
: g# Q, Z6 p3 {配置未更新的情况下,输出3次, e; w: H& W1 g/ t
MailService{mailConfig=MailConfig{username='df321543-8ca7-4563-993a-bd64cbf50d53'}}
! S% x- u$ w- X6 J5 hMailService{mailConfig=MailConfig{username='df321543-8ca7-4563-993a-bd64cbf50d53'}}! Y$ ^# m! A5 P+ \! M, B3 f
MailService{mailConfig=MailConfig{username='df321543-8ca7-4563-993a-bd64cbf50d53'}}
* `6 b3 }. x$ N模拟3次更新配置效果
* e' [) j. G2 j: j( W. gMailService{mailConfig=MailConfig{username='6bab8cea-9f4f-497d-a23a-92f15d0d6e34'}}
% P, I; W. ^9 @5 mMailService{mailConfig=MailConfig{username='581bf395-f6b8-4b87-84e6-83d3c7342ca2'}}$ q- t) A% V& H, ]4 l
MailService{mailConfig=MailConfig{username='db337f54-20b0-4726-9e55-328530af6999'}}
) A- M1 o z/ I0 c% k; b! L" g+ T2 q上面MailService输出了6次,前3次username的值都是一样的,后面3次username的值不一样了,说明修改配置起效了。
+ ?0 u6 g4 N1 o2 H q1 L4 O3 Q; p) ]
小结
- X; x' l' d7 c1 B& k) r' M& T4 ], `/ q7 L4 R t$ B0 _& `) |& q
动态@Value实现的关键是@Scope中proxyMode参数,值为ScopedProxyMode.DEFAULT,会生成一个代理,通过这个代理来实现@Value动态刷新的效果,这个地方是关键。
0 S$ Q1 E7 m% `1 z( V) L" l7 A+ m Q& Z
有兴趣的可以去看一下springboot中的@RefreshScope注解源码,和我们上面自定义的@RefreshScope类似,实现原理类似的。
" q- r$ U2 H" P/ ~9 y {. \ ~" c1 e$ ?& u
总结
8 e9 ~ _" ]' l* y$ F8 h
' v$ {$ v" _* {5 q本次面试过程中3个问题,我们都搞定了,希望你也已经掌握了,有问题的欢迎给我留言,交流!
2 k% |1 u2 |) _; ?
/ y6 o4 F* f4 g9 i! B案例源码; Q9 J0 Q" i; d) a$ @
4 L7 a. `# L8 [# |$ G1 uhttps://gitee.com/javacode2018/spring-series5 `( F4 f" E" n6 _* ]
路人甲java所有案例代码以后都会放到这个上面,大家watch一下,可以持续关注动态。* Q: @4 Q8 P" u' J) V1 a A
- ?" f) [7 Z7 I7 LSpring系列
7 t0 Z$ I$ W3 R) v2 l l& P4 Z4 Q4 i* ^) L& H2 G# ?$ w+ Q6 n
Spring系列第1篇:为何要学spring?
, ^* v5 n' w r5 y+ F6 b8 F' q7 C- @) y
Spring系列第2篇:控制反转(IoC)与依赖注入(DI)
! Z4 \4 K6 V, E" G+ D4 B& }$ n5 J" S' M* h3 B* H1 |& |
Spring系列第3篇:Spring容器基本使用及原理
4 L8 R, U$ R5 f
5 r& n1 p0 w/ [, Q8 dSpring系列第4篇:xml中bean定义详解(-)
5 D6 ~! r3 [6 }: h2 p ~2 M8 ]
$ [" S% `2 v2 o; ^' HSpring系列第5篇:创建bean实例这些方式你们都知道?% G: r2 W& `$ l2 i d4 C7 P
- T0 l; A. l0 V1 l; dSpring系列第6篇:玩转bean scope,避免跳坑里!, w1 f6 q! ?, F( }9 L9 Y
. H+ u! W/ t) @7 v$ e
Spring系列第7篇:依赖注入之手动注入
" C' z3 f9 _2 L- I1 e- u* w: q+ g1 c9 O: W
Spring系列第8篇:自动注入(autowire)详解,高手在于坚持
! j! Q a, [4 u0 a6 E0 N1 _5 v0 K) f7 V
Spring系列第9篇:depend-on到底是干什么的?# k }0 `5 i8 t
' v6 s7 s* n2 H$ e& X5 W' x& _5 wSpring系列第10篇:primary可以解决什么问题?
8 }; S! ]) r5 [; j
, _, N- F9 ^% }Spring系列第11篇:bean中的autowire-candidate又是干什么的?9 E9 q; m8 A% `' E, @* `
3 Z! |' E3 d5 ~. u0 V" lSpring系列第12篇:lazy-init:bean延迟初始化
Y4 X9 C- n4 I. t
* T; I5 Y' f0 i7 O$ v7 L8 f e/ v! qSpring系列第13篇:使用继承简化bean配置(abstract & parent)
- o* ]! ?3 a5 ~. Y
3 @: z/ x0 k* A* d8 {% |Spring系列第14篇:lookup-method和replaced-method比较陌生,怎么玩的?( \2 | ~% [2 a- m
0 j c7 y0 m O( t/ i$ I: SSpring系列第15篇:代理详解(Java动态代理&cglib代理)?
4 M! d7 S, Q! H3 {) o V) K- ^+ [6 m0 C# F
Spring系列第16篇:深入理解java注解及spring对注解的增强(预备知识)( U) t4 O, T, e. ^9 @- h6 c3 B ?) i) M
: ]2 h h* e& i2 i W
Spring系列第17篇:@Configration和@Bean注解详解(bean批量注册)
2 c$ ~& z1 q* ^% x7 w6 z1 Q) E( N0 g8 E' C7 A0 g8 d$ e2 ]
Spring系列第18篇:@ComponentScan、@ComponentScans详解(bean批量注册). g$ X, E" o+ X: M' J' T
6 p6 ]; e) n0 J; {/ O2 B1 ~
Spring系列第18篇:@import详解(bean批量注册)
3 `! z8 ]( _8 u8 ? `* y; [& D8 J7 ~( u; X0 A9 [
Spring系列第20篇:@Conditional通过条件来控制bean的注册
& _ v, F) }, z+ R1 c0 `
8 C) s3 h2 x" Z# g% J0 s( a- DSpring系列第21篇:注解实现依赖注入(@Autowired、@Resource、@Primary、@Qulifier)0 w% Q0 \. f0 ]( \% ~
d6 \$ a6 b: M0 ?Spring系列第22篇:@Scope、@DependsOn、@ImportResource、@Lazy 详解
1 k$ H7 p1 ]8 E5 d1 M( E
. x3 m5 T0 E, [* ?- XSpring系列第23篇:Bean生命周期详解
" n2 o$ O2 h6 p3 h) z2 E0 F) p' }4 k8 \3 m7 S1 G
Spring系列第24篇:父子容器详解
( ~ ]$ N' u* p% C. h( x8 f
) x% W) t" w7 K4 F' {' C更多好文章% F! d4 J; R5 w
* h4 n0 b+ K% w
Java高并发系列(共34篇)! Z5 C+ p" W3 h: m$ Z. i
o. d2 _# o; D3 b3 O
MySql高手系列(共27篇)
! l7 K& R) A8 L" t6 l0 l( D5 V; E/ b! e4 ?0 C8 m/ p: y; B" K8 J
Maven高手系列(共10篇)
) p3 }; _! N4 {- F$ E: P: p/ B6 z0 Q. r- Y) S( C+ N# ^
Mybatis系列(共12篇)
/ h! B) o, ?! K9 D3 B
$ S. u) o: p0 J3 `聊聊db和缓存一致性常见的实现方式
) G0 B/ |! ?8 g9 `# ^
4 i) E5 y5 t6 Z% z+ h接口幂等性这么重要,它是什么?怎么实现?, }* q5 D6 R: d# ~5 q9 _
2 F4 H6 ?" b; l9 y7 B泛型,有点难度,会让很多人懵逼,那是因为你没有看这篇文章!
- n7 E' M6 N+ w. Z. d" _————————————————
6 `1 p. X: m% J5 L8 K版权声明:本文为CSDN博主「路人甲Java」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。4 M# W4 y) Q% h" _) P9 h& B
原文链接:https://blog.csdn.net/likun557/article/details/105648757
: G: D8 L# Q- I9 k" c
+ G/ Z! `0 W/ }5 y3 z9 ?4 ^" l: Y- o8 E
|
zan
|