??????點(diǎn)進(jìn)來你就是我的人了
博主主頁:??????戳一戳,歡迎大佬指點(diǎn)!歡迎志同道合的朋友一起加油喔??????
目錄
前言
1.?Spring 攔截器
1.1?自定義攔截器
1.2 將自定義攔截器加入到系統(tǒng)配置中
1.3?攔截器實(shí)現(xiàn)原理
統(tǒng)一訪問前綴添加 (擴(kuò)展)
2. 統(tǒng)一異常的處理 (@ControllerAdvice 和 @ExceptionHandler)
3.統(tǒng)一數(shù)據(jù)返回格式
實(shí)現(xiàn)統(tǒng)一數(shù)據(jù)返回格式的功能
特殊情況,返回String類型
解決方案
前言
在學(xué)習(xí)了SpringAOP的原生操作后,我就立即著手想著去寫一個統(tǒng)一處理用戶登陸權(quán)限驗(yàn)證的功能。可能大多數(shù)小伙伴的想法和我一致,直接使用SpringAOP的前置通知方法或者環(huán)繞通知方法來實(shí)現(xiàn)不就行了嗎?但是在真正使用原生SpringAOP對該功能進(jìn)行實(shí)現(xiàn)時,我遇到了以下幾個問題:
- 首先是要驗(yàn)證用戶的登陸狀態(tài),就要先獲取到內(nèi)存中的session對象,但是通過前置或者環(huán)繞通知的方式時很難拿到請求對象的,也就很難拿到session對象進(jìn)行判斷。
- 其次是與我們用戶相關(guān)的控制器中并非所有方法都要進(jìn)行攔截判斷(像登錄、注冊方法),那這樣就大大增加了通過原生SpringAOP的切點(diǎn)表達(dá)式配置攔截規(guī)則的難度
那該怎么解決上述的問題呢?——更好的解決辦法就是使用Spring攔截器~
1.?Spring 攔截器
對于上述問題, Spring 提供的攔截器就可以很好地解決.
Spring攔截器與傳統(tǒng)AOP的關(guān)系可以類比于Servlet與Spring之間的關(guān)系。攔截器在某種程度上包含了傳統(tǒng)AOP的概念,同時內(nèi)置了對HTTP請求和響應(yīng)對象的支持,以提供更方便的功能。
一個項(xiàng)目里面實(shí)現(xiàn)統(tǒng)一用戶驗(yàn)證登錄的處理, 一般有三種解決方案:
- 使用傳統(tǒng)的 AOP,
- 使用攔截器,
- 使用過濾器.
既然有三種解決方案, 為什么要選擇使用攔截器呢 ?
- ?對于傳統(tǒng)的 AOP, 功能比較簡單, 寫法過于復(fù)雜, 所以不使用.
- ?對于過濾器 (web容器提供的), 因?yàn)樗膱?zhí)行時機(jī)太靠前了, Spring 框架還沒初始化, 也就是說觸發(fā)過濾器的時候, request, response 對象還沒有實(shí)例化. 所以過濾器用的也比較少.?
??實(shí)現(xiàn)攔截器的兩大步驟
-
創(chuàng)建自定義攔截器, 實(shí)現(xiàn) HandlerInterceptor 接口并重寫preHandle (執(zhí)行方法前的預(yù)處理) 方法.
-
將自定義攔截器加入 WebMvcConfigurer 的 addInterceptors 方法中. 【配置攔截規(guī)則】
- a) 給當(dāng)前的類添加
@Configuration
注解- b) 實(shí)現(xiàn)
WebMvcConfigurer
接口- c) 重寫
addInterceptors
方法
1.1?自定義攔截器
/**
* 攔截器
*/
@Component
public class LoginInterceptor implements HandlerInterceptor {
//調(diào)用目標(biāo)方法執(zhí)行之前的方法
//此方法返回的是boolean 類型的值
//如果返回的true 表示(攔截器)驗(yàn)證成功, 繼續(xù)走后續(xù)的流程,執(zhí)行目標(biāo)方法
//如果返回false, 表示攔截器執(zhí)行失敗, 驗(yàn)證為通過, 后續(xù)的流程和目標(biāo)方法就不要執(zhí)行了
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//用戶登錄判斷業(yè)務(wù)
HttpSession session = request.getSession(false);
if(session != null && session.getAttribute("session_userinfo") != null) {
//用戶已經(jīng)登錄
return true;
}
//登錄失敗,頁面返回一個錯誤狀態(tài)碼
response.setStatus(401);
return false;
}
}
自定義的攔截器是一個普通的類, 如果返回 true, 才會繼續(xù)執(zhí)行后續(xù)代碼.
1.2 將自定義攔截器加入到系統(tǒng)配置中
前面寫的自定義攔截器, 只是一個普通的類, 需要把它加入到系統(tǒng)配置中, 并配置攔截規(guī)則, 才是一個真正有用的攔截器.
@Configuration
public class myConfig implements WebMvcConfigurer {
@Autowired //屬性注入(注入loginInterceptor對象)
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**") //攔截所有url
.excludePathPatterns("/user/login") //排除url /user/login 登錄不攔截
.excludePathPatterns("/user/reg") //注冊不攔截
.excludePathPatterns("/image/**") //排除image 文件夾下的所有文件
;
}
}
1. addInterceptor 方法的作用 : 將自定義攔截器添加到系統(tǒng)配置中.
2. addPathPatterns : 表示需要攔截的 URL.
3. excludePathPatterns : 表示不攔截, 需要排除的 URL.
4. 攔截器不僅可以攔截方法, 還可以攔截靜態(tài)文件 (.png, .js, .css)?
咱們定義個UserController,用于驗(yàn)證我們自定義攔截器的功能:
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/login")
public String login() {
return "登錄成功";
}
@RequestMapping("/reg")
public String reg() {
return "注冊成功";
}
@RequestMapping("/index")
public String index() {
return "其他方法";
}
}
前兩個步驟我們已經(jīng)做好了準(zhǔn)備工作, 并配置好了攔截規(guī)則, 規(guī)定除了登錄和注冊功能不攔截外, 攔截其他所有 URL (getInfo). 下面來進(jìn)行驗(yàn)證一下攔截器是否生效.
?通過瀏覽器顯示結(jié)果和抓包結(jié)果來看, 我們自定義的攔截器確實(shí)攔截了 index() 方法, 并且設(shè)置了狀態(tài)碼 401, 這也符合我們的預(yù)期.?
1.3?攔截器實(shí)現(xiàn)原理
有了攔截器之后,會在調(diào)用Controller之前進(jìn)行相應(yīng)的業(yè)務(wù)處理,流程如下:
- 首先我們要知道 Controller 的執(zhí)行都會通過一個調(diào)度器 (DispatcherServlet) 來實(shí)現(xiàn).
- 隨便訪問 controller 中的一個方法就能在控制臺的打印信息就能看到, 這個可以類比到線程的調(diào)度上.
?然后所有 Controller 中方法都會執(zhí)行 DispatcherServlet 中的調(diào)度方法 doDispatch().
我們通過分析源碼, 發(fā)現(xiàn)源碼中的這兩個主要步驟.預(yù)處理的過程就 和 前邊代碼 LoginInterceptor 攔截器做的事情差不多,判斷攔截的方法是否符合要求, 如果符合要求, 就返回 true,然后繼續(xù)執(zhí)行后續(xù)業(yè)務(wù)代碼, 否則, 后面的代碼都不執(zhí)行.
?進(jìn)入 applyPreHandle() 方法繼續(xù)分析:
我們發(fā)現(xiàn)源碼中就是通過遍歷存放攔截器的 List, 然后不斷判斷每一個攔截器是否都返回 true 了, 但凡其中有一個攔截器返回 false, 后面的攔截器都不要走了, 并且后面的業(yè)務(wù)代碼也不執(zhí)行了. 看到這, 我們恍然大悟了.
通過前面的分析, 我們就能發(fā)現(xiàn) Spring 中的攔截器其實(shí)就是封裝了傳統(tǒng)的 AOP , 它也是通過 動態(tài)代理的和環(huán)繞通知的思想來實(shí)現(xiàn)的????????
統(tǒng)一訪問前綴添加 (擴(kuò)展)
如果我們想要在所有的請求地址前面加一個地址,我們可以進(jìn)行以下操作,我們以加前綴api為例
@Configuration
public class MyConfig implements WebMvcConfigurer {
// 所有的接口添加 api 前綴
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.addPathPrefix("api", c -> true);
}
}
我們addPathPrefix的第二個參數(shù)是一個表達(dá)式,設(shè)置為true表示啟動前綴
?在瀏覽器輸入url,測試結(jié)果如下:
2. 統(tǒng)一異常的處理 (@ControllerAdvice 和 @ExceptionHandler)
為什么要統(tǒng)一異常的處理呢 ??
? ? 就拿用戶在銀行取錢這件事來說, 如果用戶在辦理業(yè)務(wù)的時候, 后端程序報錯了, 它不返回任何信息, 或者它返回的信息不統(tǒng)一, 這都會讓前端程序猿不知道咋辦, 他不知道咋辦, 那么就無法給用戶提供相應(yīng)的提示. 此時用戶見程序沒反應(yīng), 他自己也會懷疑是自己沒點(diǎn)到, 還是程序出 bug 了. 所以需要進(jìn)行統(tǒng)一異常的處理.?
實(shí)現(xiàn)統(tǒng)一異常的處理是需要兩個注解來實(shí)現(xiàn)的:
- @ControllerAdvice : 定義一個全局異常處理類,用于處理在Controller層中拋出的各種異常,并對這些異常進(jìn)行統(tǒng)一的處理。使用@ControllerAdvice注解可以將異常處理邏輯從Controller中解耦,提高代碼復(fù)用性。
- @ExceptionHandler : 定義異常處理方法,使用@ExceptionHandler,可以根據(jù)不同類型異常進(jìn)行處理
二者結(jié)合表示, 當(dāng)出現(xiàn)異常的時候執(zhí)行某個通知 (執(zhí)行某個方法事件)
1.建立統(tǒng)一異常處理類,并加入@ControllerAdvice注解
@ControllerAdvice //表示這個類將被用于全局的異常處理
@ResponseBody //表示該類返回的是json數(shù)據(jù)
public class MyExceptionAdvice {
}
2.定義異常處理方法,使用@ExceptionHandler,可以根據(jù)不同類型異常進(jìn)行處理
我們來進(jìn)行一個空指針異常處理:
@ControllerAdvice //表示這個類將被用于全局的異常處理
@ResponseBody //表示該類返回的是json數(shù)據(jù)
public class MyExceptionAdvice {
//這個注解表示當(dāng)應(yīng)用程序中發(fā)生NullPointerException時,會調(diào)用此方法進(jìn)行處理
@ExceptionHandler(NullPointerException.class)
//這個方法返回一個HashMap,其中包含了異常處理的結(jié)果。結(jié)果以鍵值對的形式存儲,鍵是字符串,值是Object對象。
public HashMap<String,Object> doNullPointerException(NullPointerException e) {
HashMap<String ,Object> result = new HashMap<>();
result.put("code",-1); //"code":異常的狀態(tài)碼,這里是-1,表示發(fā)生了錯誤
//"msg":異常的信息,這里添加了"空指針"的前綴,并附加上異常的具體信息(e.getMessage()獲取異常的信息)。
result.put("msg","空指針" + e.getMessage());
//data":這里設(shè)置為null,表示沒有返回的數(shù)據(jù)。
result.put("data", null);
return result;
}
}
定義一個UserController?類:
@RestController //它結(jié)合了@Controller和@ResponseBody兩個注解的功能。
//這個類級別的注解指定了這個類處理的所有請求的URL路徑的公共部分
@RequestMapping("/user")
public class UserController {
@RequestMapping("/login")
public int login() {
Object obj = null;
//因?yàn)閛bj是null,所以當(dāng)調(diào)用hashCode方法時,會拋出NullPointerException。
System.out.println(obj.hashCode());
return 1;
}
}
我們再來訪問login,看是什么情況:
?為了看出區(qū)別,我們來做兩個測試:
1.首先我們?nèi)サ鬇ControllerAdvice注解
- ?我們發(fā)現(xiàn)直接報了500的錯誤信息了
2.如果不是空指針異常,而是算術(shù)異常呢
- ?我們發(fā)現(xiàn)同樣還是報了500的錯誤信息,因?yàn)槲覀兘y(tǒng)一異常處理只處理了空指針異常
- 這種情況,我們需要再寫一個算術(shù)處理異常的處理類,我們可以直接使用所有異常類的父類Exception進(jìn)行異常處理,這樣所有的異常都能處理到了
@ExceptionHandler(Exception.class)
public HashMap<String,Object> doException(Exception e) {
HashMap<String ,Object> result = new HashMap<>();
result.put("code",-1); //"code":異常的狀態(tài)碼,這里是-1,表示發(fā)生了錯誤
//"msg":異常的信息,這里添加了"空指針"的前綴,并附加上異常的具體信息(e.getMessage()獲取異常的信息)。
result.put("msg","Exception" + e.getMessage());
//data":這里設(shè)置為null,表示沒有返回的數(shù)據(jù)。
result.put("data", null);
return result;
}
- 我們再來進(jìn)行測試:
3.統(tǒng)一數(shù)據(jù)返回格式
為什么需要進(jìn)行統(tǒng)一數(shù)據(jù)返回格式:
- 方便前端程序員更好的接收和解析后端數(shù)據(jù)接口返回的數(shù)據(jù)
- 降低前端程序員和后端程序員的溝通成本,按照某個格式進(jìn)行
- 有利于項(xiàng)目統(tǒng)一數(shù)據(jù)的維護(hù)和修改
- 后端的統(tǒng)一規(guī)范的標(biāo)準(zhǔn)制定
實(shí)現(xiàn)統(tǒng)一數(shù)據(jù)返回格式的功能
實(shí)現(xiàn)步驟可以分為以下兩步:
- ? ? 自定義統(tǒng)一數(shù)據(jù)返回處理類,標(biāo)注上@ControllerAdvice注解同時實(shí)現(xiàn)ResponseBodyAdvice接口。
- ? ? 重寫接口中的 supports方法和beforeBodyWrite方法 并在該方法中進(jìn)行統(tǒng)一數(shù)據(jù)格式的處理。
?
實(shí)現(xiàn)代碼如下:
- supports決定是否執(zhí)行beforeBodyWrite(數(shù)據(jù)重寫),返回true表示重寫,false表示不重寫
- 我們這里假設(shè)標(biāo)準(zhǔn)的數(shù)據(jù)格式是HashMap
//定義的全局響應(yīng)體建議(Response Body Advice)。
//其作用是在返回數(shù)據(jù)到客戶端之前,對返回的數(shù)據(jù)進(jìn)行處理或修改
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
//是否執(zhí)行 beforeBodyWrite 方法,true = 執(zhí)行, 重寫返回結(jié)果
@Override
//supports方法,用于決定是否對返回的數(shù)據(jù)進(jìn)行處理
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
//返回數(shù)據(jù)之前進(jìn)行數(shù)據(jù)重寫
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
//Hash<String, Object> -> code,msg,data
if(body instanceof HashMap) {
return body;
}
//重寫返回結(jié)果, 讓其返回一個統(tǒng)一的數(shù)據(jù)格式
HashMap<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("data", body);
result.put("msg", "");
return result;
}
}
我們寫一下Contoller,試著返回兩組數(shù)據(jù):
@RestController
@RequestMapping("/user")
public class UserController {
}
1.HashMap格式數(shù)據(jù):
@RequestMapping("/reg")
public HashMap<String, Object> reg() {
HashMap<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("msg", "");
result.put("data", 1);
return result;
}
?2.返回其他格式數(shù)據(jù):
@RequestMapping("/login1")
public int login1() {
return 1;
}
?我們可以發(fā)現(xiàn)即使我們在Controller層返回的不是標(biāo)準(zhǔn)格式的數(shù)據(jù),也會進(jìn)行重寫。
特殊情況,返回String類型:
@RequestMapping("/sayHi")
public String sayHi() {
return "say hi";
}
?這里報了類型轉(zhuǎn)換異常,HashMap不能轉(zhuǎn)換為String,為什么會出現(xiàn)這個問題呢,我們返回String會進(jìn)行三個步驟:
- 方法返回String
- 統(tǒng)一數(shù)據(jù)格式返回的是 -> String 轉(zhuǎn)為 HashMap
- 將HashMap轉(zhuǎn)換為application/json字符串給前端
那么問題到底出現(xiàn)在哪一步呢?答案是我們在進(jìn)行類型轉(zhuǎn)換時出錯了
- 對于String 類型的數(shù)據(jù) -> 會使用 StringHttpMessageConverter 這個轉(zhuǎn)換器 進(jìn)行類型轉(zhuǎn)換(由于這個轉(zhuǎn)換器 無法將HashMap轉(zhuǎn)換為String,所以會報錯)
- 對于非String 類型的數(shù)據(jù) ->會使用 HttpMessageConverter 這個轉(zhuǎn)換器 進(jìn)行類型轉(zhuǎn)換
?
解決方案:
1. 將 StringHttpMessageConverter 轉(zhuǎn)換器去掉,那么在進(jìn)行類型轉(zhuǎn)換的時候就會使用?HttpMessageConverter ?這個轉(zhuǎn)換器了
@Configuration
public class MyConfig implements WebMvcConfigurer {
@Override
//configureMessageConverters 方法使用 removeIf 方法刪除了所有 StringHttpMessageConverter 的實(shí)例。
// 這樣,Spring MVC 就不會再使用 StringHttpMessageConverter 來處理 String 類型的數(shù)據(jù)了。
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.removeIf(converter -> converter instanceof StringHttpMessageConverter);
}
}
?2.在統(tǒng)一數(shù)據(jù)重寫時,單獨(dú)處理String類型,直接讓其返回一個json格式的字符串,而不是HashMap
//返回數(shù)據(jù)之前進(jìn)行數(shù)據(jù)重寫
@SneakyThrows
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
if(body instanceof HashMap) {
return body;
}
//重寫返回結(jié)果, 讓其返回一個統(tǒng)一的數(shù)據(jù)格式
HashMap<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("data", body);
result.put("msg", "");
if(body instanceof String) {
//將String類型的字符串轉(zhuǎn)換成json格式的字符串返回
return objectMapper.writeValueAsString(result);
}
return result;
}
}
今天的內(nèi)容到處就結(jié)束了! 如果你覺得這篇文章有價值,或者你喜歡它,那么請點(diǎn)贊并分享給你的朋友。你的支持是我創(chuàng)作更多有用內(nèi)容的動力,感謝你的閱讀和支持。祝你有個美好的一天!"文章來源:http://www.zghlxwxcb.cn/news/detail-489940.html
文章來源地址http://www.zghlxwxcb.cn/news/detail-489940.html
到了這里,關(guān)于【Spring Boot 】Spring Boot 統(tǒng)一功能處理的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!