SpringBoot 統(tǒng)一功能的處理
1. 用戶登錄權(quán)限校驗
1.1 最初用戶登錄驗證
@RestController
@RequestMapping("/user")
public class UserController {
/**
* 某?法 1
*/
@RequestMapping("/m1")
public Object method(HttpServletRequest request) {
// 有 session 就獲取,沒有不會創(chuàng)建
HttpSession session = request.getSession(false);
if (session != null && session.getAttribute("userinfo") != null) {
// 說明已經(jīng)登錄,業(yè)務(wù)處理
return true;
} else {
// 未登錄
return false;
}
}
/**
* 某?法 2
*/
@RequestMapping("/m2")
public Object method2(HttpServletRequest request) {
// 有 session 就獲取,沒有不會創(chuàng)建
HttpSession session = request.getSession(false);
if (session != null && session.getAttribute("userinfo") != null) {
// 說明已經(jīng)登錄,業(yè)務(wù)處理
return true;
} else {
// 未登錄
return false;
}
}
// 其他?法...
}
從上述代碼中可以看出每個方法都相同的登錄權(quán)限校驗 , 這樣做的缺點是:
- 每個方法中都要單獨寫用戶登錄驗證的方法 , 即使封裝成公共方法 , 也一樣要在方法中傳參判斷.
- 添加控制器越多, 調(diào)用用戶登錄的方法也越多 , 這樣后期會增大維護(hù)成本.
- 用戶登錄方法與接下來的業(yè)務(wù)實現(xiàn)沒有任何關(guān)聯(lián) , 但還是要每個方法中寫一遍.
因此, 使用 AOP 思想, 進(jìn)行統(tǒng)一用戶登錄驗證迫在眉睫.
1.2 Spring AOP 統(tǒng)一用戶登錄驗證的問題
一說到用戶登錄驗證 , 第一個想到的方法就是 , Spring AOP 前置或環(huán)繞通知來實現(xiàn) , 具體實現(xiàn)代碼如下:
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class UserAspect {
// 定義切點?法 controller 包下、?孫包下所有類的所有?法
@Pointcut("execution(* com.example.demo.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 {
// 執(zhí)?攔截?法
obj = joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
System.out.println("Around ?法結(jié)束執(zhí)?");
return obj;
}
}
但是在 Spring AOP 的切面中實現(xiàn)用戶登錄校驗有以下兩個缺點:
- 沒法獲取到 HttpSession 對象
- 由于需要攔截一部分方法 , 另一部分是不攔截的 , 如注冊和登錄方法不攔截 , 這樣的話排除規(guī)則將無法定義.
1.3 SpringAOP 攔截器
Spring 中提供了具體的實現(xiàn)攔截器 HandlerInterceptor , 攔截器的實現(xiàn)分為以下兩個步驟:
- 創(chuàng)建自定義攔截器 , 實現(xiàn) HandlerInterceptor 接口的 preHandle(執(zhí)行具體方法之前的預(yù)處理) 方法.
- 將自定義攔截器加入 WebMvcConfigurer 的 addInterceptors 方法中.
具體實現(xiàn)如下:
1.3.1 實現(xiàn)自定義攔截器
//定義攔截器
@Component
public class LoginInterceptor implements HandlerInterceptor {
// 調(diào)用目標(biāo)方法之前執(zhí)行的方法
// 此方法返回 boolean 類型的值 , 如果返回值為 true, 繼續(xù)執(zhí)行剩余流程, 否則表示攔截器驗證未通過, 剩余的不在執(zhí)行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession(false);
if (session != null || session.getAttribute("session_userinfo") != null){
return true;
}
//如果執(zhí)行失敗不能直接給前端返回一個狀態(tài)碼, 后端必須明確告訴前端異常信息, 但狀態(tài)碼必須是200,
//原理類似于確認(rèn)應(yīng)答, 如果是異常狀態(tài)碼前端無法接收到信息.
response.setContentType("application/json;charset=utf8");
response.getWriter().println("{\"code\":-1, \"msg\":\"登錄失敗\", \"data\":\"\"}");
return false;
}
}
1.3.2 將自定義攔截器加入到系統(tǒng)配置
@Configuration
public class MyConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/user/login")//排除登錄
.excludePathPatterns("/user/reg");//排除注冊
}
}
其中:
- addPathPatterns() 表示需要攔截的 URL
- excludePathPatterns() 表示需要排除的 URL
1.4 攔截器實現(xiàn)原理
1.4.1 實現(xiàn)流程圖
Spring 項目中 , 正常的程序調(diào)用如下:
然而有了攔截器之后 , 就會在 Controller 之間進(jìn)行預(yù)處理操作:
1.4.2 實現(xiàn)源碼剖析
通過觀察 Spring Boot 控制臺的打印信息可知 , 所有的 Controller 執(zhí)行都會通過一個調(diào)度器 DispatcherServlet 來實現(xiàn).
所有方法都會執(zhí)行 DispatcherServlet 中的 doDispatch 調(diào)度方法 , doDispatch 源碼如下:
通過源碼可以看出 , 執(zhí)行 Controller 之前, 會先調(diào)用預(yù)處理方法 applyPreHandle() , applyPreHandle() 源碼如下:
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
for(int i = 0; i < this.interceptorList.size(); this.interceptorIndex = i++) {
//獲取所有攔截器, 并調(diào)用preHandle()方法
HandlerInterceptor interceptor = (HandlerInterceptor)this.interceptorList.get(i);
if (!interceptor.preHandle(request, response, this.handler)) {
this.triggerAfterCompletion(request, response, (Exception)null);
return false;
}
}
return true;
}
通過源碼可以看出 , applyPreGHandle() 會獲取所有攔截器 HandlerInterceptor 并執(zhí)行其中的 preHandle()方法 , 由此就與上文中的攔截器定義相對應(yīng).
通過上述源碼分析 , 攔截器也是通過動態(tài)代理和環(huán)繞通知是思想實現(xiàn)的 , 大體流程如下:
1.5 統(tǒng)一訪問前綴添加
在企業(yè)開發(fā)中 , 如果我們的項目工程較大且多個項目部署到同一臺服務(wù)器上 , 如果不給具體的項目添加前綴 , 那么就會極大的增加維護(hù)成本.
eg. 給當(dāng)前項目所有請求地址添加 api 前綴:
@Configuaration
public class AppConfig implement WebMvcConfigurer(){
@Override
public void configurePathMatch(PathMatchConfigure configure){
configure.addPathPrefix("api",c -> true)
}
}
第二個參數(shù)為表達(dá)式 , 設(shè)置 true 表示啟動前綴.
那么后續(xù)訪問時 , URL 都需要加上 api 前綴.
2. 統(tǒng)一異常處理
統(tǒng)一異常處理使用的是 @ControllerAdvice + @ExceptionHandler 來實現(xiàn)的 , @ControllerAdvice 表示控制器通知類 , @ExceptionHandler 表示異常處理器 , 兩個結(jié)合表示出現(xiàn)異常時執(zhí)行某個通知 , 也就是執(zhí)行某個方法事件 , 具體實現(xiàn)代碼如下:
無論后端執(zhí)行結(jié)果如何 , 都會給前端返回一個明確的信息.
2.1 創(chuàng)建一個異常處理類
import java.util.HashMap;
@ControllerAdvice//針對 Controller 的增強(qiáng)方法, 會檢測控制器的異常
public class MyExceptionAdvice{
}
2.2 創(chuàng)建異常檢測的類和異常處理方法
import java.util.HashMap;
@ControllerAdvice//針對 Controller 的增強(qiáng)方法, 會檢測控制器的異常
@ResponseBody //返回非靜態(tài)頁面 (數(shù)據(jù))
public class MyExceptionAdvice{
@ExceptionHandler(NullPointerException.class)
public HashMap<String, Object> doNullPointerException(NullPointerException e){
HashMap<String, 0bject> result = new HashMap<>();
result.put("code", -1);
result.put("msg", "空指針: " + e.getMessage());
result.put("data", null);
return result;
}
//默認(rèn)異常處理, 當(dāng)具體異常匹配不到時, 執(zhí)行此方法
@ExceptionHandler(Exception.class)
public HashMap<String, Object> doException(Exception e){
HashMap<String, 0bject> result = new HashMap<>();
result.put("code", -300);
result.put("msg", "Exception: " + e.getMessage());
result.put("data", null);
return result;
}
}
3. 統(tǒng)一數(shù)據(jù)返回
3.1為什么需要統(tǒng)一數(shù)據(jù)返回?
- 方便前端程序員更好的接收和解析數(shù)據(jù)接口返回的數(shù)據(jù)
- 降低前后端溝通成本
- 有利于統(tǒng)一的數(shù)據(jù)維護(hù)和修改
- 有利于后端技術(shù)部門統(tǒng)一標(biāo)準(zhǔn)的規(guī)定
保底策略 , 強(qiáng)制性統(tǒng)一數(shù)據(jù)返回 , 返回數(shù)據(jù)之前進(jìn)行數(shù)據(jù)重寫
3.2 統(tǒng)一數(shù)據(jù)返回格式的實現(xiàn)
統(tǒng)一返回數(shù)據(jù)的格式可以使用 @ControllerAdvice + ResponseBodeyAdvice 的方式實現(xiàn) , 實現(xiàn)代碼如下:
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
//只有 true 時, 才會執(zhí)行 beforeBodyWriter()
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
//返回數(shù)據(jù)之前對數(shù)據(jù)進(jìn)行重寫
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
//首先判斷是否已經(jīng)是標(biāo)準(zhǔn)格式了
if (body instanceof HashMap){
return body;
}
// 重寫返回結(jié)果, 讓其返回一個統(tǒng)一的數(shù)據(jù)格式
HashMap<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("msg", null);
result.put("data", body);
return result;
}
}
Tips: 實際開發(fā)中 , 通常不建議將 HashMap 作為返回類型 , 因為使用 HashMap 作為返回類型,無法提供類型信息,容易導(dǎo)致數(shù)據(jù)解析錯誤或類型轉(zhuǎn)換異常 , 可讀性差 , 維護(hù)困難.
3.3 統(tǒng)一異常處理在遇到 String 返回類型時報錯的問題
當(dāng)返回類型是 String 時
@RequestMapping("/login")
public String login(){
return "login";
}
控制臺拋出異常:
如果剖析一下返回執(zhí)行流程:
- 方法返回的是 String
- 統(tǒng)一數(shù)據(jù)返回之前處理 ----> String 轉(zhuǎn)換為 HashMap
- 將 HashMap 轉(zhuǎn)換為 application/json 字符串給前端
通過抓包可以看出 , 返回給前端的是 json 格式的數(shù)據(jù) , 因此異常出現(xiàn)在第三步.
第三步轉(zhuǎn)換時 , 首先查看原 Body 的數(shù)據(jù)類型:
- 是 String --> 調(diào)用 StringHttpMessageConverter 進(jìn)行類型轉(zhuǎn)換
- 非 String --> 調(diào)用 HttpMessageConverter 進(jìn)行類型轉(zhuǎn)換
總而言之 , 原本是 HashMap 類型的數(shù)據(jù) , 卻被判斷成 String 類型的數(shù)據(jù) , 并調(diào)用 StringHttpMessageConverter 進(jìn)行類型轉(zhuǎn)換 , 于是就出現(xiàn)了 HashMap cannot be cast to java.lang.String
解決方案:
- 通過修改配置文件將 StringHttpMessageConverter 這個轉(zhuǎn)換器從項目中去除.
- 在統(tǒng)一數(shù)據(jù)重寫時 , 單獨處理 String 類型 , 讓其返回一個 String 字符串 , 而非 HashMap
解決方案一:
@Configuration
public class MyConfig implements WebMvcConfigurer {
/**
* 移除 StringHttpMessageConverter()
* @param converters
*/
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.removeIf(converter -> converters instanceof StringHttpMessageConverter);
}
}
解決方案二:
@Autowired
private ObjectMapper objectMapper;
@SneakyThrows
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
//首先判斷是否已經(jīng)是標(biāo)準(zhǔn)格式了
if (body instanceof HashMap){
return body;
}
// 重寫返回結(jié)果, 讓其返回一個統(tǒng)一的數(shù)據(jù)格式
HashMap<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("msg", null);
result.put("data", body);
if (body instanceof HashMap){
// 返回一個 String 字符串
objectMapper.writeValueAsString(result);
}
return result;
}
3.4 ControllerAdvice 源碼剖析
點擊 @ControllerAdvice 實現(xiàn)源碼如下:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice {
@AliasFor("basePackages")
String[] value() default {};
@AliasFor("value")
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
Class<?>[] assignableTypes() default {};
Class<? extends Annotation>[] annotations() default {};
}
從上述源碼中可以看出 @ControllerAdvice 派生于 @Component 組件 , 而所有的組件初始化都會調(diào)用 InitializingBean 接口.
通過查詢 InitializingBean , 可以發(fā)現(xiàn)其中 Spring MVC 實現(xiàn)子類是 RequestMappingHandlerAdapter , 里面有一個 afterPropertiesSet() 方法 , 表示所有參數(shù)設(shè)置完成之后執(zhí)行的方法.
package org.springframework.beans.factory;
public interface InitializingBean {
void afterPropertiesSet() throws Exception;
}
在 afterPropertiesSet() 中有一個 initControllerAdviceCache 方法, 此方法的源碼如下:
分析可知 , 該方法會查找所有的 @ControllerAdvice 類 , 這些類未被存入容器中 , 但發(fā)生某個時間時 , 會調(diào)用相應(yīng)的 Advice 方法 , 比如返回數(shù)據(jù)前調(diào)用統(tǒng)一數(shù)據(jù)封裝.
gHandlerAdapter , 里面有一個 afterPropertiesSet() 方法 , 表示所有參數(shù)設(shè)置完成之后執(zhí)行的方法.
package org.springframework.beans.factory;
public interface InitializingBean {
void afterPropertiesSet() throws Exception;
}
在 afterPropertiesSet() 中有一個 initControllerAdviceCache 方法, 此方法的源碼如下:
[外鏈圖片轉(zhuǎn)存中…(img-mo8rbC9p-1689386373868)]文章來源:http://www.zghlxwxcb.cn/news/detail-569694.html
分析可知 , 該方法會查找所有的 @ControllerAdvice 類 , 這些類未被存入容器中 , 但發(fā)生某個時間時 , 會調(diào)用相應(yīng)的 Advice 方法 , 比如返回數(shù)據(jù)前調(diào)用統(tǒng)一數(shù)據(jù)封裝.文章來源地址http://www.zghlxwxcb.cn/news/detail-569694.html
到了這里,關(guān)于SpringBoot 統(tǒng)一功能的處理的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!