高并发秒杀系统方案的优化 ; }( q- [+ G/ ~, m% n ) ], ]2 b* ?* N$ z& `" h1 a; s最近接触了一个关于高并发秒杀的项目,在这里稍微整理一下关于这个项目的一些值得记录的一些点,以下是源码地址:github |1 a) U# }. }0 `1 b
高并发项目的瓶颈主要在于数据库访问次数上,访问次数越多,对数据库压力也就越大,因此项目中主要也是通过redis进行缓存以及RabbitMQ进行请求入队缓冲等来减少对数据库的访问。$ f0 }- I7 O4 K8 q# N
高并发秒杀$ v8 N3 B4 }: N& G6 [7 g6 G- v
6 j8 {7 o' B( T3 d
一、项目结构搭建) s% H! Y( d ^# y9 D' y$ g
1、集成Thymeleaf+ }/ Y/ M' }3 f8 k/ j
2、集成Redis7 k% E2 c. T. \
3、JMeter的使用 " B# `) R. g* x, V4、Result的封装 1 V# }- P, o" y1 T: D3 _/ v二、功能实现 * d* o9 W) Q4 M+ j/ \1 Y U1、全局异常控制器 + T, P) R8 ?% J) H2、拦截器的使用9 O: }* a d5 ]' \: a
3、分布式Session的使用+ D' F, ^& m& P1 Q
三、页面缓存优化3 K( E* y. s2 L/ i# _8 h
1、页面缓存4 ~$ T# h( _7 Z
2、对象缓存 : _& U6 f4 \6 \3、页面静态化 5 ?' ] V, Y- O* E q# H四、接口优化 / V0 O; s# }* x+ e7 A$ }五、安全优化, C$ p" U# w# e. @: W, o
1、秒杀接口地址隐藏 2 _9 ?$ u8 L4 ^& \: p3 t2、数学公式验证码3 z% B# h( o( B7 T3 t
3、接口防刷+ t! I. V& ]2 \4 v1 ~1 Y( w1 t
4、用户存储 ) G. h, A! `- p/ x8 ?六、个人感受4 F( L ~' t8 a$ e' }
一、项目结构搭建$ N/ t% {: [- E3 C8 [ Q
% o' p) z4 J+ ~5 i7 Q
1、集成Thymeleaf5 T! f/ p8 ^" E; ^
# o! p! \6 p3 |4 d
Thymeleaf是一个Java库,是一个XML/XHTML/HTML5模板引擎,能够应用于转换模板文件,以显示应用程序产生的数据和文本。 v- U# U# O3 a4 Z: |* EThymeleaf旨在提供⼀个优雅的、⾼度可维护的创建模板的⽅式。 为了实现这⼀⽬标,Thymeleaf建⽴在⾃然模板的概念上,将其逻辑注⼊到模板⽂件中,不会影响模板设计原型。 这改善了设计的沟通,弥合了设计和开发团队之间的差距。" ]6 _4 f9 W- L( p, W2 K) s
Thymeleaf是一个类似于JSP的模板引擎,但他的区别在于,在运行项目之前,Thymeleaf也是纯HTML,可以在没有服务端的情况下进行运行,但JSP需要在服务端的支持下进行一定的转换。 3 O5 u+ v& H* f* J' | L4 T: W$ YThymeleaf主要有以下三个优点:7 @/ C$ I2 m! R* p
& T8 G! V$ e; ^' z" g! OThymeleaf不管在有网络或者无网络的情况下都可以运行,因为它支持html原型,通过在html标签中增加额外的属性来达到模板+数据的展示方式,当有数据返回到页面时,Thymeleaf标签会动态地替换掉静态内容,使页面动态显示。3 O' m8 g& r8 Z; E/ S c" P
Thymeleaf 开箱即用的特性。它提供标准和spring标准两种方言,可以直接套用模板实现JSTL、 OGNL表达式效果,避免每天套模板、该jstl、改标签的困扰。同时开发人员也可以扩展和创建自定义的方言。 4 D* g( ~; L9 c. f: \8 QThymeleaf 提供spring标准方言和一个与 SpringMVC 完美集成的可选模块,可以快速的实现表单绑定、属性编辑器、国际化等功能.- n5 ]2 K; u' V! I
//集成thymeleaf d7 r! s( U3 m: m! ^ N/ d w: x
<dependency># z) {& D, ]0 Y4 B/ Z
<groupId>org.springframework.boot</groupId>, |: @$ K+ @( A) s
<artifactId>spring-boot-starter-thymeleaf</artifactId>2 [0 U! d0 p6 u" X+ y: z: Z1 ^
<version>1.5.2.RELEASE</version>* r/ [) K2 L# F4 Q8 F
</dependency> $ O! ?4 }" c2 @! S( F1% o* i3 c" [( l* c8 b3 M
2. b7 C, B0 W& m! n! D& A& @
35 J7 R4 w1 l% Q7 t K( f# v8 A2 s
4 . [& U4 X& \; d$ |0 e5$ X5 E t, i" i0 C8 Y
6 5 ]+ U$ ^$ r+ c4 ?$ A0 r& b2、集成Redis 8 |" m4 W' r8 N' D, i- X4 h" \$ Z6 f7 Z+ s9 ~9 \$ W. }* z& |) u |
redis的集成,其中使用jedisPool获取jedis,并使用jedis的方法封装在RedisService当中,方便后来使用redis进行缓存" i; r: j- J' t' `% b2 L1 D
3 g7 }% B' m/ F
/** 2 G8 B3 [8 ?# p E * 设置对象2 h; z" A2 [" r& A' \+ f; e5 Y
* @param prefix 不同的前缀对应不同的缓存信息8 b- \/ @. `' t" f. G: L
* @param key 该信息具体的的key值& u! ]9 c' d8 U
* @param value : f( a7 a6 ^0 i, T. W, b ? * @param <T> . ?) c+ E0 ]* z- Z( P# J( q* n9 { * @return - W- S2 x: C/ C$ P. k1 J4 b( d */5 O% c; C/ A( h; C" n) ]% |
public <T> boolean set(KeyPrefix prefix,String key,T value){3 d2 m+ F% T+ @! M4 Q9 r' ~- L0 C
Jedis jedis=null;; A: {- G3 b/ M# v1 A4 M& n
try{ K$ z( T3 a/ `. O: L/ t jedis=jedisPool.getResource(); & U2 f. d; g: y. P+ I& X String str=beanToString(value);9 K9 ~ N1 N7 V% S
if(str==null||str.length()<=0) return false; * w7 N# u" o, E( v4 A" u5 k( ^% m //获取有前缀的key! d2 a: } g* ]& J+ D$ z0 B
String realKey=prefix.getPrefix()+key;! C; F) F- y" W# [4 | n& W
int seconds=prefix.expireSeconds();! w; d7 t- h+ k$ l J. w
if(seconds<=0){ : g. j# v5 j1 x jedis.set(realKey,str); 9 r; w/ ^' X& _ }else{/ W7 z F, `$ w( \+ j2 j
jedis.setex(realKey,seconds,str);. |; k4 n" M& T! m) T
} , _" [8 U7 J- a0 {7 B return true;- k7 W9 I5 S/ H1 C! K
}finally{* x g# m6 R* d
returnToPool(jedis); 8 b- G; H; l7 J( [3 N2 i }( G9 }# M( i6 ?; F; a0 i# Q
}& ~ y3 B+ R: V
18 j# z6 ] g6 u1 Q4 ~0 N$ ~
28 \; `# D4 o# v) H
3 . T" [, k j7 S) p6 B1 X4 ; @2 L' i6 ~3 m5 ( u( A" ~ z% O0 p67 X5 D. U# C0 K
7 3 A1 n0 ?( G/ S J. ~# A+ Z8* Q2 X$ _0 D& o9 Z% b0 m! k
9 . z2 X# d8 P5 o, t10" E7 k- ]' K* O9 Y$ A8 _9 i
11& w% C: \% G" j# }
12 x8 l( X* W) [/ x* S3 Z
133 M7 u" L# y% r0 e
149 \6 P* E1 r) H1 J2 w, @" k
15' ^( g/ U0 x% I
16 . @1 Z0 k5 O* C* s" i17- T! j) d% } q2 {/ M
184 r& _( ^! M5 D+ g/ x o7 K: S7 l
193 K" K0 R( }7 G; q) P) m6 |
20 0 g$ S# i- G5 y21, `9 [3 c9 ] E1 ~- m
22 ) W6 e$ d( N3 z/ ~0 |23 / B1 E* C( e7 Y' {7 S7 f24 u) X. y- R2 E6 i0 D v
25' T9 C6 m+ {& e1 i8 ^
26 8 d2 ]# b) o2 f7 d, d( @, z27$ m9 Y+ V1 b2 n8 z+ G) m6 f! K
而前缀是为了分辨不同的缓存,例如商品缓存、订单缓存、用户缓存等等,其中的expireSeconds则是缓存的有效时间,0代表永久有效。2 g2 P& l1 @( q" S
4 R3 s1 S. T0 o: y
public class GoodsKey extends BasePrefix{ % s( l" E: f( Y8 d2 Z private GoodsKey(int expireSeconds,String prefix) { 4 D* D2 X5 P1 A3 A, P- `. y0 ~2 ~ super(expireSeconds,prefix);1 x0 w& O# O' o4 U# \1 t! L
}$ O# c/ k0 u2 r3 x) m+ G
public static GoodsKey getGoodsList=new GoodsKey(60,"gl"); 4 x; s" Q5 w2 T" B public static GoodsKey getGoodsDetail=new GoodsKey(60,"gd");, _0 x& ~+ x( R( @# e; f( w
public static GoodsKey getMiaoshaGoodsStock=new GoodsKey(0,"gs"); . r5 Y% M( g4 ?- w8 U" l) ^3 ~5 a5 B0 l/ _5 z
} $ F% P$ N$ z2 C9 b3 w( o1 & t% U, o0 \: a. f2 8 e6 b' M7 j1 S. b- C3 8 C. J' g9 E4 w, e1 E47 @$ K( P1 o2 s/ [/ g
5 " M# \6 Q. O/ ~" ^: `6 j: U6& a9 `, e' y6 y4 X3 i# a' I2 [) ~
7& u p3 A% L) d
8. d% E8 n: B# G( u
95 d# K( m ~+ _8 o, I* \( A
3、JMeter的使用 ' i0 A V! \3 ?( }% T( F e3 i" y+ y/ j V
Apache JMeter是Apache组织开发的基于Java的压力测试工具。用于对软件做压力测试,JMeter是一个下载即用的软件,我们使用它来进行高并发的模拟,而它会在进行模拟后给予我们一些可视化的结果,是一个很强大的工具。似乎在5.的版本中,已经不需要去配置JMETER_HOME就可以运行了。在使用过程中,最大的感想就是这个软件巨坑,不知道是不是硬件的问题,在进行500010的并发压测中,出现大量的error,而每次出现error的情况都不尽相同,让人很是头疼。3 i+ m2 y# u+ b' O
' p- ?4 l0 w4 v/ ~4、Result的封装 8 o( N x& s" n; } w& s1 `0 L1 ]0 Q% B+ J8 r
这是对每次访问结果的一个封装,而CodeMsg类则是对返回码和信息的封装,而Result则是对CodeMsg和对data的封装,在访问过后,我们可以选择生成Result.success对象返回任何类型的信息,异或是返回Result.error来返回错误码和错误信息。 - s- ?0 Z& [3 q& ]0 w w S4 @& k7 T* r
public class Result<T>{ / t W3 s. C% Y+ W private int code;; |/ E0 y2 _; R
private String msg; 2 G8 T+ x; @7 D. ~/ S$ @ private T data;! a; B; |6 G: ?2 W! f! [
d- p1 E; H2 \
/*** ]6 d3 i8 n6 o( c/ f: b
* 成功时候的调用 ! H" G% c& I1 O& y */ / l. c8 |* V" S% Z public static <T> Result<T> success(T data){ - A2 {" i' [, I w; R1 b, E5 M, m return new Result<T>(data);2 H- J& G! h9 V2 e& o8 H* s5 s7 ]" H
}6 N' Y* b; X. h7 M6 U, q
/** , T1 V! |/ L' [- Y$ y" {5 x( E3 N * 失败时候的调用 4 e8 ~' b) c0 j6 ~( K */ 4 e* r9 F F! {8 g7 n public static <T> Result<T> error(CodeMsg codeMsg){" E3 ^, s) S4 ^+ k/ G) n
return new Result<T>(codeMsg); 6 @6 }( L$ P. f3 @. c- q: T } . \2 A, q$ f9 \3 r4 e8 C8 ?$ v: i9 J
% j. i" ?0 w- e+ G, O; Z private Result(T data){0 T: v/ r( X8 y. p
this.data=data;. C* S. }% S! x5 `0 y( r- E
}% `: q3 o. `# R
private Result(int code,String msg){ T6 h2 q( a) a+ g" @6 M; R$ \
this.code=code;* B2 @" r0 @( G) Z
this.msg=msg; * n/ Y# n& o1 s' B5 C$ a( O } ( X, D% f' O# z( d5 |6 ~( E) J private Result(CodeMsg codeMsg){ $ p* N4 \5 g% u) a! y5 M if(codeMsg!=null){ 1 E# |8 b* } ^0 e5 X0 C; i5 }" V% v this.code=codeMsg.getCode();! c V9 I0 R& w5 Y
this.msg=codeMsg.getMsg(); " \4 J5 N5 `: u5 Q! \4 Z8 X } 6 ^' i1 w6 a# x# D }7 e9 o4 M7 ~8 R7 L
8 T( Q8 O% u6 G q/ f public int getCode() { 5 k$ A" l- i3 o Q return code;/ f( z! k# E, M7 o9 J3 v1 J7 B) b
} N9 I( R* r8 R9 M% V/ ~ public void setCode(int code) { 9 y1 G: e- S2 p3 [' W8 J! i this.code = code;, {( F! G* d3 j+ Q
}+ u% E) ]& r1 d. v
public String getMsg() {9 g$ u# o: p) J
return msg; * v# f$ w/ P$ l }7 b/ E/ w7 _0 t/ U4 k3 h: O8 ^: e" u
public void setMsg(String msg) { l: x' S" C1 L+ H8 X0 o3 o5 F this.msg = msg; E( M3 k. h' y } q. K6 ~# h* s6 z7 z( o public T getData() { ) j6 A& z9 V' _9 v, N8 ~9 ] return data; 9 C# j4 s$ { o5 S! W } $ {. N9 F6 X" _ B$ z9 t6 R( R$ X public void setData(T data) {( J) C6 h1 V9 H+ y/ ^% ^
this.data = data; $ o" Y, t1 u" T } % D! K/ J d( J} ) I& L% Y4 \/ _8 ?; @1 " ` v1 A) X* z7 U" r! p1 {# O1 p2/ E6 X$ x0 _. V: j( {3 [
31 w: E3 R# H, _0 O
4" s9 \9 Q7 B( P0 u# Y8 G
5 & T1 S0 ~4 T" `% n6 I3 X- l; x. H% ?7$ {" k, D5 ~# j0 p$ F/ u& D F
88 R3 Y2 t* D1 T
9 ( d. a) B% k9 ?) ^10 ! `7 \3 n# m. T. ?11 * O9 H k) S0 j m12 % m) q& V4 `2 Y# R# Q" a; [13! y' u4 B l, M/ R/ F- u
14 + r: D6 ?$ y9 x j% B; h& W& i3 C, E15* W n# f: \" w" S
16 7 D `# {1 w" v1 z& Y$ y17 $ W" }; t7 Q0 E" j! f m18 % ?1 y5 p, L0 g0 f$ N: A7 \19 ( r& `5 v% b9 N- K3 q20 , I; c @+ D: D3 J9 l" R21 ! W; \" J8 \ U. V# r6 H1 G0 a22 ! p& t8 g- s9 M4 A* T8 ]' q23 $ s2 n1 h% y6 F" }9 C2 z3 G6 d+ I24 S; h8 U: C) j# i( Z6 Y- l
25 ; K4 O- j b6 Y+ k/ b26' b, ]2 [6 e+ a$ p/ v2 @
27 - }, r9 [# S# W6 V) j28- g! A6 f7 }/ U
29/ `7 {# a. Q! `8 M8 t
30 / `7 [4 B) s6 M# G0 c) _31 : w6 G+ B7 Q& T32$ e* s3 X% _* ^- n7 B! ^
338 K, E( v- c3 ~( f: |6 b
34) M4 e$ n- _1 s# M& i$ V h
35 + @5 R8 R( ^3 e- ?7 R4 c5 R36 2 Q: m8 }: Y+ R; S# P& k- ~375 y: h9 Q( i- |6 H9 i
38 ; X5 g- m: I2 g; [; m39 . w5 [& g1 `4 E. C) l& \/ x+ Y3 Z40 ( r: R! x; c. {) f" r% L' H$ q41 6 }. V6 V2 [' l; A7 T% w7 m42 / ~9 A1 S' V% x# ]8 _4 |43 {; V6 y- U6 U
44 : W1 c5 y5 N2 @* ]: `$ k3 |45/ ^% C4 D$ ^) e V/ B2 x+ O
46 : X R6 I- P4 _7 }47; ^1 |4 S! }! B' I
48 " c/ n2 J& {! H7 l: S49 ! L3 c% K* A; |506 g4 `; \% \& r
519 L0 l! u( [5 C6 G
52* o- l6 m- M6 R% E6 t- W: ?. Y
二、功能实现 4 X/ \7 G; v# V! Q$ ` # S2 Z( ~" _: r6 Y1 B功能实现这一块算是比较常规的实现,毕竟这个项目的重心在于秒杀的优化而不在于具体的实现,只是实现了常规的登录,商品列表,商品秒杀等功能。# a% z# ?! c. h, [0 k4 A. f
; k; ], T6 f8 a6 T- h( d) F9 i; U
1、全局异常控制器 9 r A5 ?& n8 q f4 N' t: V4 W4 L为了使得代码更清爽而不十分复杂,我们实现了一个全局异常控制器。在这个类中,@ControllerAdvice是一个增强的Controller注解,使用这个Controller,可以实现以下功能:全局异常处理、全局数据绑定、全局数据预处理(SpringMVC提供,在SpringBoot中可以直接使用)@ExceptionHandler:如果只使用此注解,只能在当前Controller中处理异常,当配合ControllerAdvice一起使用的时候,就可以摆脱这种限制了。 7 [- l4 Z: G" L( q* s Y0 R' X; m: b! G8 Q' H, T" h
import com.yuan.SecondsKill.result.CodeMsg;* X! I, S4 L+ y$ w: W0 ?8 L
import com.yuan.SecondsKill.result.Result;/ d( k& Z0 r8 N% X
import org.springframework.validation.BindException; + ?) H% `$ s7 @! I8 f& jimport org.springframework.validation.ObjectError; ) k! S& K" |7 k j, l- m- ]import org.springframework.web.bind.annotation.ControllerAdvice;* x; y/ Q& ~* K/ A/ e
import org.springframework.web.bind.annotation.ExceptionHandler; & N; k7 [$ W0 S+ v6 Jimport org.springframework.web.bind.annotation.ResponseBody; & E2 Z+ D9 v- c, \7 [2 b* Vimport javax.servlet.http.HttpServletRequest; 9 n* z# n/ E# R( ?, G# Nimport java.util.List;8 J- E3 x, `. p \4 e
3 [; H2 F( C: [4 l' u& O3 P/** 3 Y! O. M- F9 \* a1 b& r* P5 b4 S * 全局异常控制器 0 d0 D2 N. e3 y% t5 c N& ]' @8 E" K& R * 拦截异常,在页面上显示错误信息( T, }5 y* W4 _4 v
*/ 4 _0 f8 T8 w. j) c8 ^@ControllerAdvice: `9 W1 I0 F- S! K5 {/ W
@ResponseBody ; G0 s) n/ T4 } P4 b; cpublic class GlobalExceptionHandler { 3 ^8 D5 o5 B+ b/ K, I3 b4 R @ExceptionHandler(value = Exception.class) / c, O2 c0 N; W5 h( e0 p7 F public Result<String> exceptionHandler(HttpServletRequest request,Exception e){% X( K4 o1 y Z2 @* Y
e.printStackTrace();5 H" o; u* E1 Z, ]0 v$ W1 {# U
if(e instanceof GlobalException){ 1 S4 g0 N3 N( L GlobalException ge=(GlobalException)e; ' O5 h# ~' @# @# S- I4 _; D return Result.error(ge.getCm()); : e' d7 v8 y8 J: |; K' G$ j: A }3 p0 U/ B: D2 G: Y8 k) s
if(e instanceof BindException){ 8 a# K# |' o6 [+ K BindException ex=(BindException)e; # Y5 J) L# p" K2 z) \% ^1 b List<ObjectError> errors = ex.getAllErrors(); 9 P* d: w$ q& @0 w* u8 S) [ ObjectError error = errors.get(0); # }. k& `1 O2 V e- J# ]5 e String msg=error.getDefaultMessage();, z- |& w. k; n* e, d8 {+ L3 X
return Result.error(CodeMsg.BIND_ERROR.fillArgs(msg));7 w# Y% b2 G# S% h
}else{ $ d- K, D( S" E/ h! _ return Result.error(CodeMsg.SERVER_ERROR);' j! W1 Y* L. t) f# S0 \# G
}$ t) G% e. ]3 @, |; X6 W* A0 u
}& z( v3 E, @4 v
} * [+ E) x& ~$ C2 S1) N* d$ i/ o' W) A6 S
2 0 `* |4 {6 ?: G) h! d3 |2 l37 V1 |' s# x U
4 2 C, A6 T+ U7 n f6 v7 q6 n5 / ?& \2 _% H, d2 \: y6 4 w' F2 E' Z, P7 M0 A7 # X; W* ?/ P0 d8 % X H1 d8 \9 [2 X+ d! c/ v X9 - ?$ X0 h P; [8 g10 : O! I2 U# [* O9 e11& q( d& b; n. E
12 0 k! F! \- J i y5 U) O13 + G [" T h- g4 H, Z. y/ d, [: O14: L! e; j7 N; H$ l+ E4 V
15 9 ]* `; \) R2 n$ E5 W' {8 `163 c# g q0 i& s3 A
17 & E' [) a s) e1 T }18 s6 s. J8 r& n, a) m
19+ u9 L. D* n j( j
20* ]% c: _8 I) x
21 $ \8 L h( O6 v7 L22 : T" O+ K* F# [+ l: W, s23 3 U& M; v& K) q247 K" D" u1 l, x7 N/ ?' b
25 ( k# D4 r( Y% L# Y26# ]+ z/ v2 @ y& u; l
27# M7 I; Z, M( J0 @2 y
28 . X) r% z: X2 k: u7 F29 ! T3 L) j/ `) x+ _3 ] l30! A$ P e/ o. r/ j( [. \% z
31 4 d& |. S5 {+ | w. d" _$ \0 b9 O! D32 9 ~ B* H9 i, g33: l0 B' u$ n# e8 A$ `
34/ F& V: q n" k& D
35 ) R0 G0 {( s' P# C' H在登陆方法中,使用@Valid注解配合全局处理器达到对手机号和密码进行格式校验的目的 ' S* S2 B+ s7 h( ]# t# Z) E" e& w6 p4 b$ D4 z- Z0 }+ E$ Z: i
@RequestMapping("/do_login")" B0 O! k6 R: i
@ResponseBody7 s1 a- E; B W* W) n6 b
public Result<String> doLogin(HttpServletResponse response, @Valid LoginVo loginVo){& d' T; _0 T& F* w" r4 N
log.info(loginVo.toString()); 7 H2 @. S) {9 f: x return null; . V. @; x/ v& x" L0 z7 x } x' V4 E+ F' o1 ?9 j1 1 H3 X) M8 l; z3 ]2+ n9 F/ C; _8 e; y! Z8 r. U/ z
3 ) P* S- W# h8 B" j3 i5 d40 L8 [4 r P$ n1 O# F0 J
5 s7 _5 L) _. i9 a$ @& ^6 C+ n& m3 a3 I实体类中的注解来校验是否为空等等,其中的isMobile是自己定义的。! q2 E! E$ v& Z, i' [2 y
0 K% F; z, Y" I% j+ d) w6 b! @public class LoginVo { 0 @$ g" c) U2 z- m5 ? @NotNull2 u& j' ^$ c9 l: Y) \( `
@isMobile2 }6 ?, e3 G% G6 F5 H
private String mobile;9 U, t$ R0 i" K k" j ?" Y |1 _
" u7 P1 W( R7 P8 \/ c @NotNull' h8 ~( L: B, k
@Length(min=6)4 K& y9 n0 j5 j0 a& _# c0 p2 l/ T
private String password; + p/ z" t. V& p; l5 i% U8 h; h% N: k} 1 V7 c/ c% Y6 G C' {' H$ g* _11 j/ M' j; t3 p
2 % q& Z1 g+ ]* @3: q- L/ P* K( R
4 * I0 ?0 t! J8 u# k0 r$ ?54 O4 u- @8 `) n( d3 E' N
6 6 i/ }' l( P, f1 J# v7 m7+ y; ~' t3 P6 ^: d3 q2 C
82 h x* a7 C- C' o } J: Q; O
9 * S0 H5 q/ g3 k) k, k@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER}) ; V" A- d& a) v# }) R@Retention(RetentionPolicy.RUNTIME)% |6 T3 w' n+ m! N2 s0 }
@Documented . D8 K E2 |* ]8 h0 i@Constraint(validatedBy = {isMobileValidator.class}). k7 Y8 y3 h) B W0 G6 p
public @interface isMobile {3 f4 C- M% e* }/ T4 j4 I/ ` A
boolean required() default true; 1 O! Q3 V3 H9 R7 ?5 t: F( K8 ?8 J$ S/ o2 G
String message() default "手机号格式错误"; 5 q! i1 n, w. { Z* h% N4 }' }; e( |
Class<?>[] groups() default {}; 5 N- T0 U/ x! J4 z, _5 Z2 K6 `' C2 _6 M4 X- o# I* q: O0 O9 R- _
Class<? extends Payload>[] payload() default {};8 l' O: a5 N" Q. b/ V
}& m4 m- l' A# l+ u4 E
1 " Z" ], ?1 q1 X0 O/ d8 E& g2$ f1 c- S7 e. p2 V8 i0 a V
3. {5 D3 R' F9 M! f: h- P3 q8 c" Z* }
4 8 I) `& ^- H4 _( y) |) N5 ; L9 Q! l/ `3 J$ S63 F# \* R( d( }$ j c
78 Y5 t/ G% i2 y2 H) Z7 B% H
8 % p& R, y* Y c6 V9$ m) K% q0 t2 j% @3 U U
10 2 S0 W2 o2 r- d11; j: o) q, b6 J' x8 o! s8 Y. ~
129 z+ W; ^& z7 m; ^: J
13 & l5 Q( H4 f# T# i& X9 y1 U2、拦截器的使用 . @ g0 I7 L/ y7 x9 H' [0 t( \4 M1 e) n8 k# X* C
使用拦截器与实现了HandlerMethodArgumentResolver接口的类从session中获取user对象,并作为参数注入Controller中。8 o0 v( j$ r0 t* q; N
9 F. P% C) ^+ z; A* @# ~/ c( d$ s3、分布式Session的使用- c1 A% D4 C3 C# S/ F3 M
( u- W: l l5 |
此处分布式session的实现主要是在登录的过程中,为用户随机生成一个token,并保存到数据库中,随后封装成cookie返回前端,并且,获取用户也由这个cookie中的token来实现。关于分布式Session一般有4种解决方案,详细可参考这篇文章:分布式Session的4种解决方案。项目中所使用的就是其中的利用cookie记录Session。6 {* D M/ W2 X6 g V