目录
3.3.1 将 StringHttpMessageConverter 去掉。
3.3.2 在统一数据返回的时候,单独处理String类型,让其返回一个String字符串,而非 HashMap
前言:
一般Spring Boot统一功能处理模块,也是AOP的实战环节,要实现课程目标有以下3个:
- 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(){
-
- }
- // 环绕⽅法
- @Around("pointcut()")
- public Object doAround(ProceedingJoinPoint joinPoint){
- Object obj = null;
- System.out.println("Around ⽅法开始执⾏");
- try {
- // 执⾏拦截⽅法
- obj = joinPoint.proceed();
- } catch (Throwable throwable) {
- throwable.printStackTrace();
- }
- System.out.println("Around ⽅法结束执⾏");
- return obj;
- }
- }
我们知道SpringAOP虽然就提供了对用户登录的处理逻辑,但是存在一些问题:
拦截器和SpringAOP虽然都是AOP的实现方式,但是这两个其实是完全不同的技术体系。
Spring提供了具体的实现拦截器:HandlerInterceptor,该SpringBoot 拦截器实现分为以下两个步骤:
自定义拦截器继承HandlerInterceptor后,需要重写相对应的方法,这里我们重写 preHandle方法:
代码如下:
- package com.example.demo.common;
-
- import org.springframework.web.servlet.HandlerInterceptor;
-
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- import javax.servlet.http.HttpSession;
-
- /**
- * 自定义拦截器
- */
- public class LoginInterceptor implements HandlerInterceptor {
- /**
- * 以下方法是调用目标方法之前执行的方法。此方法返回boolean类型的值
- * 返回true标识验证成功,程序会继续执行后续流程
- * 返回false, 表示拦截器拦截失败, 验证未通过, 后续的流程和目标方法不再执行。
- * @param request
- * @param response
- * @param handler
- * @return
- * @throws Exception
- */
- @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;
- }
- return false;
- }
- }
重写addInterceptors方法:
设置拦截规则,代码如下
- package com.example.demo.config;
-
- import com.example.demo.common.LoginInterceptor;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
- import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
-
- public class MyConfig implements WebMvcConfigurer {
- @Autowired
- 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夹目录下的所有url都不进行拦截
- }
- }
或者使用Spring方法,通过DI注入的方式,这样可以实现不用new一个实例:
首先需要将拦截器添加到spring中,也就是给其添加一个五大类注解,这里我们就使用@Component。接着就可以使用@Autowired来得到实例。
其中:
说明:以上拦截规则可以拦截此项目中的URL,包括静态文件(图片文件,JS和CSS等文件)。
下面我们先来看一组正常情况下的调用顺序:
然而有了拦截器之后,会在调用Controller之前进行相应的业务处理,执行的流程如下图所示:
所有的Controller执行都会通过一个调度器DispatcherServlet来实现,这一点可以从Spring Boot控制台的打印信息看出,如下图所示:
在IDEA中,通过全局搜索doDispatch,方法代码如下:
- protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
- HttpServletRequest processedRequest = request;
- HandlerExecutionChain mappedHandler = null;
- boolean multipartRequestParsed = false;
- WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
-
- try {
- try {
- ModelAndView mv = null;
- Object dispatchException = null;
-
- try {
- processedRequest = this.checkMultipart(request);
- multipartRequestParsed = processedRequest != request;
- mappedHandler = this.getHandler(processedRequest);
- if (mappedHandler == null) {
- this.noHandlerFound(processedRequest, response);
- return;
- }
-
- HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
- String method = request.getMethod();
- boolean isGet = HttpMethod.GET.matches(method);
- if (isGet || HttpMethod.HEAD.matches(method)) {
- long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
- if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) {
- return;
- }
- }
-
- if (!mappedHandler.applyPreHandle(processedRequest, response)) {
- return;
- }
- // 实现Controller的业务逻辑
- mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
- if (asyncManager.isConcurrentHandlingStarted()) {
- return;
- }
-
- this.applyDefaultViewName(processedRequest, mv);
- mappedHandler.applyPostHandle(processedRequest, response, mv);
- } catch (Exception var20) {
- dispatchException = var20;
- } catch (Throwable var21) {
- dispatchException = new NestedServletException("Handler dispatch failed", var21);
- }
-
- this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
- } catch (Exception var22) {
- this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22);
- } catch (Throwable var23) {
- this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", var23));
- }
-
- } finally {
- if (asyncManager.isConcurrentHandlingStarted()) {
- if (mappedHandler != null) {
- mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
- }
- } else if (multipartRequestParsed) {
- this.cleanupMultipart(processedRequest);
- }
-
- }
- }
观察DispatcherServlet中的某段代码:
我们发现,在执行后续的Controller代码之前,都会先执行这个applyPreHandle方法,于是鼠标双击 applyPreHandle,得到代码如下:
从上述源码可以看出,在applyPreHandle中会获取所有的拦截器HandlerInterceptor并执行拦截器中的preHandle方法,这样就和之前定义的拦截器对应上了,如下图所示:
- package com.example.demo.common;
-
- import org.springframework.stereotype.Component;
- import org.springframework.web.servlet.HandlerInterceptor;
-
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- import javax.servlet.http.HttpSession;
-
- /**
- * 自定义拦截器
- */
- @Component
- public class LoginInterceptor implements HandlerInterceptor {
- /**
- * 以下方法是调用目标方法之前执行的方法。此方法返回boolean类型的值
- * 返回true标识验证成功,程序会继续执行后续流程
- * 返回false, 表示拦截器拦截失败, 验证未通过, 后续的流程和目标方法不再执行。
- * @param request
- * @param response
- * @param handler
- * @return
- * @throws Exception
- */
- @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;
- }
- response.setContentType("application/json");
- response.setCharacterEncoding("utf8");
- response.getWriter().println("asdasd");
- return false;
- }
- }
所有请求地址添加api前缀:
代码如下:
- package com.example.demo.config;
-
- import org.springframework.context.annotation.Configuration;
- import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
- import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
-
- @Configuration
- public class AppConfig implements WebMvcConfigurer {
- @Override
- public void configurePathMatch(PathMatchConfigurer configurer) {
- configurer.addPathPrefix("fox", c -> true);
- }
- }
通俗来讲,统一异常处理的主要目的是为了方便前端,让其更好的处理后端的信息,尽量将逻辑处理这块放置于后端,前端的目的其实主要是为用户服务。
比如:可以跟前端约定出现异常报错时候的状态码是多少,这样方便前端的处理,也方便后续后端在日志文件中将其找到,并修改异常。
统一异常处理使用的是@ControllerAdvice + @ExceptionHandler 来实现的,@ControllerAdvice表示控制器通知类, @ExceptionHandler是异常处理器,两个结合表示当出现异常的时候执行某个通知,也就是执行某个方法事件,具体实现代码如下:
以上方法表示,如果出现了异常就返回给前端一个HashMap对象, 其中包含的字段如代码定义那样。
注意:
方法名和返回值都是可以自定义的,另外 @ExceptionHandler()中的参数是可以选择的,这里是Exception.class:表示的是可以在程序抛出异常的时候执行这里的代码,让其返回数据给前端,如果填入的参数是NullPointerException:那么表示的是当程序出现空指针异常的时候,会执行这里的代码。
这里的实现逻辑和Java中的异常处理是相似的,如果开发者有对Exception和NullPointerException分别进行了处理,那么当程序出现NullPointerException异常的时候,还是会根据我们写的NullPointerException执行逻辑进行处理,并不会直接走Exception的逻辑。
示例如下:
访问页面后效果如下:
总结:当有多个异常通知时,匹配顺序为当前类及其子类向上依次匹配。
统一数据返回格式的优点如下,比如以下几个:
统一的数据返回格式可以使用@ControllerAdvice + ResponseBodyAdvice 的方式实现,具体实现代码如下:
- package com.example.demo.common;
-
-
- import org.springframework.core.MethodParameter;
- import org.springframework.http.MediaType;
- import org.springframework.http.server.ServerHttpRequest;
- import org.springframework.http.server.ServerHttpResponse;
- import org.springframework.web.bind.annotation.ControllerAdvice;
- import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
-
- import java.util.HashMap;
-
-
- /**
- * 统一数据格式处理
- */
- @ControllerAdvice
- public class ResponseAdvice implements ResponseBodyAdvice {
-
- /**
- * 是否执行 beforeBodyWrite 方法, 返回 true 就执行, 返回 false 就不执行
- * @param returnType
- * @param converterType
- * @return
- */
- @Override
- public boolean supports(MethodParameter returnType, Class converterType) {
- return true;
- }
-
- /**
- * 返回数据之前进行数据重写
- * @param body 原始返回值
- * @param returnType
- * @param selectedContentType
- * @param selectedConverterType
- * @param request
- * @param response
- * @return
- */
- @Override
- public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
- // 这里我们规定统一的数据返回为HashMap
- if (body instanceof HashMap) {
- return body;
- }
- // 重写返回结果,让其返回一个统一的数据格式
- HashMap
result = new HashMap<>(); - result.put("code",200);
- result.put("data",body);
- result.put("msg","");
- return result;
- }
- }
访问user/login1:
经过统一功能处理后代码展现如下:
但是如果将返回值改为String类型,按照以上的执行逻辑,那么就无法走上述的正常数据统一处理:
我们发现,当返回类型为String的时候,程序会抛出异常,从而被我们的 统一异常处理模块拦截。
观察异常信息,发现 抛出异常:java.lang.ClassCastException: java.util.HashMap cannot be cast to java.lang.String
可能会感到奇怪,为什么会抛出这个异常呢?
下面我们来看看后端返回前端时候的执行流程:
1. 一开始,前端访问该网址时,方法返回的是 String:
2. 统一数据返回之前会进行处理,将 String 转换为 HashMap:
3. 将HaspMap转换成 application/json 字符串给前端(接口)
这个步骤有两种情况,先判断原Body的类型:
以上报错就是因为原始Body是String类型,所以在类型转换时候报错了
解决方案有如下两种:
3.3.1 将 StringHttpMessageConverter 去掉。
在配置文件中使用以下代码即可;
- package com.example.demo.config;
-
- import org.springframework.context.annotation.Configuration;
- import org.springframework.http.converter.HttpMessageConverter;
- import org.springframework.http.converter.StringHttpMessageConverter;
- import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
-
- import java.util.List;
-
- @Configuration
- public class MyConfig implements WebMvcConfigurer {
- /**
- * 移除 StringHttpMessageConverter
- * @param converters
- */
- @Override
- public void configureMessageConverters(List
> converters) { - converters.removeIf(converter -> converter instanceof StringHttpMessageConverter);
- }
- }
访问地址后显示如下:
3.3.2 在统一数据返回的时候,单独处理String类型,让其返回一个String字符串,而非 HashMap
引入ObjectMapper(ObjectMapper 是Jackson库中的一个类,用于在Java对象(POJO,Plain Old Java Objects)和JSON数据之间进行相互转换):
对Body为String进行单独处理:
访问页面如下所示:
本文主要介绍了统一用户登录权限的效验,使用WebMvcConfigurer + HandlerInterceptor 来实现。统一异常处理使用 @ControllerAdvice + @ExceptionHandler 来实现,统一返回值处理使用@ControllerAdvice + ResponseBodyAdvice来处理。