目錄
1. 用戶登錄權(quán)限校驗(yàn)
1.1 最初用戶登錄權(quán)限效驗(yàn)
1.2 Spring AOP 用戶統(tǒng)?登錄驗(yàn)證
1.3 Spring 攔截器
(1)創(chuàng)建自定義攔截器
(2)將自定義攔截器添加到系統(tǒng)配置中,并設(shè)置攔截的規(guī)則
1.4 練習(xí):登錄攔截器
(1)實(shí)現(xiàn) UserController 實(shí)體類
(2)返回的登錄頁面:login.html
(3)實(shí)現(xiàn)效果
?1.5 攔截器實(shí)現(xiàn)原理
(1)實(shí)現(xiàn)原理源碼分析
1.6 統(tǒng)一訪問前綴添加
(1)在系統(tǒng)的配置文件中設(shè)置
?(2)在 application.properies 中配置
2. 統(tǒng)一的異常處理
2.1 異常的統(tǒng)一封裝
(1)創(chuàng)建一個(gè)類,并在類上標(biāo)識(shí):@ControllerAdvice
?(2)添加方法 @ExceptionHandler 來訂閱異常
3. 統(tǒng)一數(shù)據(jù)返回格式
3.1 為什么要統(tǒng)一數(shù)據(jù)返回格式
3.2 統(tǒng)一數(shù)據(jù)返回格式的實(shí)現(xiàn)
(1)創(chuàng)建一個(gè)類,并添加 @ControllerAdvice
(2)實(shí)現(xiàn) ResponseBodyAdvice 接口,并重寫 supports 和 beforeBodyAdvice 方法
4. @ControllerAdvice 源碼分析
(1) @ControllerAdvice 源碼
(2)查看 initializingBean 有哪些實(shí)現(xiàn)類
?(3)查詢 initControllerAdviceCache 方法
本節(jié)主要講解Spring Boot 統(tǒng)一功能處理,同樣也是 AOP 的實(shí)戰(zhàn)環(huán)節(jié),我們希望能夠?qū)崿F(xiàn)以下目標(biāo):
- 統(tǒng)一用戶登陸權(quán)限驗(yàn)證
- 統(tǒng)一異常處理
- 統(tǒng)一數(shù)據(jù)格式返回
1. 用戶登錄權(quán)限校驗(yàn)
回顧一下最初用戶登錄驗(yàn)證的實(shí)現(xiàn)方法:
- 最初的用戶登錄校驗(yàn)版本:在每個(gè)方法中獲取 Session 以及 Session 中的信息,對(duì)用戶賬號(hào)以及密碼進(jìn)行校驗(yàn),正確則登錄成功,反之則失敗
- 第二版本:實(shí)現(xiàn)統(tǒng)一方法去校驗(yàn)是否登陸成功,在每個(gè)需要驗(yàn)證的方法中調(diào)用統(tǒng)一的用戶登錄身份效驗(yàn)方法來判斷
- 第三版本:使用 Spring AOP 來進(jìn)行用戶統(tǒng)一登錄校驗(yàn)
- 第四版本:使用 Spring 攔截器來實(shí)現(xiàn)用戶的統(tǒng)一登錄驗(yàn)證
1.1 最初用戶登錄權(quán)限效驗(yàn)
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/a1")
public Boolean login (HttpServletRequest request) {
// 有 Session 就獲取,沒有就不創(chuàng)建
HttpSession session = request.getSession(false);
if (session != null && session.getAttribute("userinfo") != null) {
// 說明已經(jīng)登錄,進(jìn)行業(yè)務(wù)處理
return true;
} else {
// 未登錄
return false;
}
}
@RequestMapping("/a2")
public Boolean login2 (HttpServletRequest request) {
// 有 Session 就獲取,沒有就不創(chuàng)建
HttpSession session = request.getSession(false);
if (session != null && session.getAttribute("userinfo") != null) {
// 說明已經(jīng)登錄,進(jìn)行業(yè)務(wù)處理
return true;
} else {
// 未登錄
return false;
}
}
}
這種方式寫的代碼,每個(gè)方法中都有相同的用戶登錄驗(yàn)證權(quán)限,缺點(diǎn)是:
- ??? 每個(gè)方法中都要單獨(dú)寫用戶登錄驗(yàn)證的方法,即使封裝成公共方法,也一樣要傳參調(diào)用和在方法中進(jìn)行判斷
- ??? 添加控制器越多,調(diào)用用戶登錄驗(yàn)證的方法也越多,這樣就增加了后期的修改成功和維護(hù)成功
- ??? 這些用戶登錄驗(yàn)證的方法和現(xiàn)在要實(shí)現(xiàn)的業(yè)務(wù)幾乎沒有任何關(guān)聯(lián),但還是要在每個(gè)方法中都要寫一遍,所以提供一個(gè)公共的 AOP 方法來進(jìn)行統(tǒng)一的用戶登錄權(quán)限驗(yàn)證是非常好的解決辦法。
1.2 Spring AOP 用戶統(tǒng)?登錄驗(yàn)證
統(tǒng)一用戶登錄驗(yàn)證,首先想到的實(shí)現(xiàn)方法是使用 Spring AOP 前置通知或環(huán)繞通知來實(shí)現(xiàn):
@Aspect // 當(dāng)前類是一個(gè)切面
@Component
public class UserAspect {
// 定義切點(diǎn)方法 Controller 包下、子孫包下所有類的所有方法
@Pointcut("execution(* com.example.springaop.controller..*.*(..))")
public void pointcut(){}
// 前置通知
@Before("pointcut()")
public void doBefore() {}
// 環(huán)繞通知
@Around("pointcut()")
public Object doAround(ProceedingJoinPoint joinPoint) {
Object obj = null;
System.out.println("Around 方法開始執(zhí)行");
try {
obj = joinPoint.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
System.out.println("Around 方法結(jié)束執(zhí)行");
return obj;
}
}
但如果只在以上代碼 Spring AOP 的切面中實(shí)現(xiàn)用戶登錄權(quán)限效驗(yàn)的功能,有這樣兩個(gè)問題:
- ??? 沒有辦法得到 HttpSession 和 Request 對(duì)象
-
??? 我們要對(duì)一部分方法進(jìn)行攔截,而另一部分方法不攔截,比如注冊(cè)方法和登錄方法是不攔截的,也就是實(shí)際的攔截規(guī)則很復(fù)雜,使用簡單的 aspectJ 表達(dá)式無法滿足攔截的需求
?
1.3 Spring 攔截器
針對(duì)上面代碼 Spring AOP 的問題,Spring 中提供了具體的實(shí)現(xiàn)攔截器:HandlerInterceptor,攔截器的實(shí)現(xiàn)有兩步:
- ??? 創(chuàng)建自定義攔截器,實(shí)現(xiàn) Spring 中的 HandlerInterceptor 接口中的 preHandle方法
- ??? 將自定義攔截器加入到框架的配置中,并且設(shè)置攔截規(guī)則
(1)創(chuàng)建自定義攔截器
//實(shí)現(xiàn) HandlerInterceptor 接口
public class loginInterceptor implements HandlerInterceptor {
/**
* 返回 true 繼續(xù)下序流程
* false 表示驗(yàn)證失敗
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 用戶登錄業(yè)務(wù)判斷
// false 表示當(dāng)不存在 session 不存在時(shí)不需要?jiǎng)?chuàng)造一個(gè)會(huì)話信息
HttpSession session = request.getSession(false);
if (session != null && session.getAttribute("userinfo") != null){
// 說明用戶已經(jīng)登錄
return true;
}
// 可以直接跳轉(zhuǎn)到登錄頁面 或 返回一個(gè) 401、403 沒有權(quán)限碼
response.sendRedirect("/login.html");
// response.setStatus(401);
return false;
}
}
(2)將自定義攔截器添加到系統(tǒng)配置中,并設(shè)置攔截的規(guī)則
- addPathPatterns:表示需要攔截的 URL,**表示攔截所有?法
- excludePathPatterns:表示需要排除的 URL
@Configuration // 讓隨著spring啟動(dòng)而啟動(dòng)
public class AppConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new loginInterceptor())
.addPathPatterns("/**")// 攔截所有請(qǐng)求
.excludePathPatterns("/user/login")// 不攔截的 url 地址
.excludePathPatterns("/user/reg")
.excludePathPatterns("/**/*.html");
}
}
1.4 練習(xí):登錄攔截器
實(shí)現(xiàn)愿望:
- 登錄、注冊(cè)頁面不攔截,其余頁面都攔截
- 等登陸成功寫入 session 后,攔截頁面可訪問
(1)實(shí)現(xiàn) UserController 實(shí)體類
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/getUser")
public String getuser(){
System.out.println("執(zhí)行了 getUser !");
return "get user";
}
@RequestMapping("/login")
public String login(){
System.out.println("執(zhí)行了 login !");
return "get login";
}
@RequestMapping("/reg")
public String reg(){
System.out.println("執(zhí)行了 reg !");
return "get reg";
}
}
(2)返回的登錄頁面:login.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>登錄頁面</title>
</head>
<body>
<h1>登錄頁面</h1>
</body>
</html>
(3)實(shí)現(xiàn)效果
?
?1.5 攔截器實(shí)現(xiàn)原理
?
(1)實(shí)現(xiàn)原理源碼分析
- 所有的 Controller 執(zhí)行都會(huì)通過一個(gè)調(diào)度器 DispatcherServlet 來實(shí)現(xiàn)?
- 而所有方法都會(huì)執(zhí)行 DispatcherServlet 中的 doDispatch 調(diào)度?法,doDispatch 源碼分析如下:
?通過源碼分析,可以看出,Sping 中的攔截器也是通過動(dòng)態(tài)代理和環(huán)繞通知的思想實(shí)現(xiàn)的
1.6 統(tǒng)一訪問前綴添加
方法:
- 在系統(tǒng)的配置文件中設(shè)置
- 在 application.properies 中配置
(1)在系統(tǒng)的配置文件中設(shè)置
/**
* 所有的接口添加 api 前綴
* c 代表所有的請(qǐng)求(Controller)
* 表示所有的地址都會(huì)加上這個(gè)前綴
* @param configurer
*/
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.addPathPrefix("api",c -> true);
}
現(xiàn)在我們?nèi)ゲ榭粗安槐粩r截的地址
?(2)在 application.properies 中配置
?
2. 統(tǒng)一的異常處理
- 給當(dāng)前的類上加 @ControllerAdvice 表示控制器通知類
- 給方法上添加 @ExceptionHandler(xxx.class),表示異常處理器,添加異常返回的業(yè)務(wù)代碼
我們先去制造些異常:
?
2.1 異常的統(tǒng)一封裝
(1)創(chuàng)建一個(gè)類,并在類上標(biāo)識(shí):@ControllerAdvice
@ControllerAdvice
public class ExceptionHandler {
}
?(2)添加方法 @ExceptionHandler 來訂閱異常
@ControllerAdvice
@ResponseBody// 表示當(dāng)前的所有方法返回的都是數(shù)據(jù)不是頁面
public class ExHandler {
/**
* 攔截所有的空指針異常,繼續(xù)統(tǒng)一的數(shù)據(jù)返回
*/
@ExceptionHandler(NullPointerException.class)// 空指針異常
public HashMap<String,Object> nullException(NullPointerException e){
HashMap<String,Object> result = new HashMap<>();
result.put("code","-1");
result.put("msg","空指針異常:" + e.getMessage());//錯(cuò)誤碼的描述信息
result.put("date",null);
return result;
}
}
但是需要考慮的一點(diǎn)是,如果每個(gè)異常都這樣寫,那么工作量是非常大的,并且還有自定義異常,所以上面這樣寫肯定是不好的,既然是異常直接寫 Exception 就好了,它是所有異常的父類,如果遇到不是前面寫的兩種異常,那么就會(huì)直接匹配到 Exception
當(dāng)有多個(gè)異常通知時(shí),匹配順序?yàn)楫?dāng)前類及其?類向上依次匹配
@ControllerAdvice
@ResponseBody// 表示當(dāng)前的所有方法返回的都是數(shù)據(jù)不是頁面
public class ExHandler {
/**
* 攔截所有的空指針異常,繼續(xù)統(tǒng)一的數(shù)據(jù)返回
*/
@ExceptionHandler(NullPointerException.class)// 空指針異常
public HashMap<String,Object> nullException(NullPointerException e){
HashMap<String,Object> result = new HashMap<>();
result.put("code","-1");
result.put("msg","空指針異常:" + e.getMessage());//錯(cuò)誤碼的描述信息
result.put("date",null);
return result;
}
@ExceptionHandler(Exception.class)// 所有異常
public HashMap<String,Object> AllException(NullPointerException e){
HashMap<String,Object> result = new HashMap<>();
result.put("code","-1");
result.put("msg","異常:" + e.getMessage());//錯(cuò)誤碼的描述信息
result.put("date",null);
return result;
}
}
?
?
3. 統(tǒng)一數(shù)據(jù)返回格式
3.1 為什么要統(tǒng)一數(shù)據(jù)返回格式
- 方便前端程序員更好的接收和解析后端數(shù)據(jù)接口返回的數(shù)據(jù)。
- 降低前端程序員和后端程序員的溝通成本,按照某個(gè)格式實(shí)現(xiàn)就行了,因?yàn)樗薪涌诙际沁@樣返回的
- 有利于項(xiàng)目統(tǒng)一數(shù)據(jù)的維護(hù)和修改。
- 有利于后端技術(shù)部門的統(tǒng)一規(guī)范的標(biāo)準(zhǔn)制定,不會(huì)出現(xiàn)稀奇古怪的返回內(nèi)容。
3.2 統(tǒng)一數(shù)據(jù)返回格式的實(shí)現(xiàn)
(1)創(chuàng)建一個(gè)類,并添加 @ControllerAdvice
@ControllerAdvice
public class ResponseAdvice {
}
(2)實(shí)現(xiàn) ResponseBodyAdvice 接口,并重寫 supports 和 beforeBodyAdvice 方法
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
/**
* 表示是否需要重寫
* 返回true則執(zhí)行beforeBodyWrite方法,反之則不執(zhí)行
*/
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
@Override
public Object beforeBodyWrite(Object body,
MethodParameter returnType,
MediaType selectedContentType,
Class selectedConverterType,
ServerHttpRequest request,
ServerHttpResponse response) {
HashMap<String,Object> hashMap = new HashMap<>();
hashMap.put("code",200);// 狀態(tài)碼
hashMap.put("msg","");// 錯(cuò)誤的描述信息
hashMap.put("date",body);
return hashMap;
}
}
supports方法相當(dāng)于是一個(gè)開關(guān),只有當(dāng) true 時(shí)才能執(zhí)行重寫 beforeBodyWrite 方法,false就不重寫
當(dāng)訪問 getUser 時(shí)發(fā)生異常了,類型訪問異常?
注意:
我們知道String既不屬于基本數(shù)據(jù)類型,又不屬于對(duì)象,且在重寫方法的時(shí)候其余類型都是用的統(tǒng)一的格式化工具,而String用的是它自身的格式化工具,String自身的格式化工具在執(zhí)行的時(shí)候還沒有加載好,就會(huì)導(dǎo)致 原始類型 是String的時(shí)候,在轉(zhuǎn)化成HashMap的時(shí)候就會(huì)報(bào)錯(cuò)
所以在統(tǒng)一返回的時(shí)候需要對(duì)String進(jìn)行單獨(dú)的處理
?
jackson就是用于 json 數(shù)據(jù)轉(zhuǎn)換的,json的轉(zhuǎn)換工具?
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
HashMap<String,Object> hashMap = new HashMap<>();
hashMap.put("code",200);// 狀態(tài)碼
hashMap.put("msg","");// 錯(cuò)誤的描述信息
hashMap.put("date",body);
if (body instanceof String){
// 判斷數(shù)據(jù)類型是不是 String,是String需要特殊處理,因?yàn)?String 在轉(zhuǎn)換的時(shí)候會(huì)報(bào)錯(cuò)
try {
return objectMapper.writeValueAsString(hashMap);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
return hashMap;
}
4. @ControllerAdvice 源碼分析
通過對(duì) @ControllerAdvice 源碼的分析我們可以知道上面統(tǒng)一異常和統(tǒng)一數(shù)據(jù)返回的執(zhí)行流程
(1) @ControllerAdvice 源碼
可以看到 @ControllerAdvice 派生于 @Component 組件而所有組件初始化都會(huì)調(diào)用 InitializingBean 接口
(2)查看 initializingBean 有哪些實(shí)現(xiàn)類
在查詢過程中發(fā)現(xiàn),其中 Spring MVC 中的實(shí)現(xiàn)子類是 RequestMappingHandlerAdapter,它里面有一個(gè)方法 afterPropertiesSet()方法,表示所有的參數(shù)設(shè)置完成之后執(zhí)行的方法
?(3)查詢 initControllerAdviceCache 方法
文章來源:http://www.zghlxwxcb.cn/news/detail-653971.html
發(fā)現(xiàn)這個(gè)方法在執(zhí)行時(shí)會(huì)查找使用所有的 @ControllerAdvice 類,發(fā)送某個(gè)事件時(shí),調(diào)用相應(yīng)的 Advice 方法,比如返回?cái)?shù)據(jù)前調(diào)用統(tǒng)一數(shù)據(jù)封裝,比如發(fā)生異常是調(diào)用異常的 Advice 方法實(shí)現(xiàn)的文章來源地址http://www.zghlxwxcb.cn/news/detail-653971.html
到了這里,關(guān)于Spring Boot 統(tǒng)一功能處理(攔截器實(shí)現(xiàn)用戶登錄權(quán)限的統(tǒng)一校驗(yàn)、統(tǒng)一異常返回、統(tǒng)一數(shù)據(jù)格式返回)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!