网站建设服务图片,google关键词seo,wordpress edd 卡密,wordpress修改网页SpringBoot 统一功能的处理 文章目录 SpringBoot 统一功能的处理1. 用户登录权限校验1.1 最初用户登录验证1.2 Spring AOP 统一用户登录验证的问题1.3 SpringAOP 拦截器1.3.1 实现自定义拦截器1.3.2 将自定义拦截器加入到系统配置 1.4 拦截器实现原理1.4.1 实现流程图1.4.2 实现…SpringBoot 统一功能的处理 文章目录 SpringBoot 统一功能的处理1. 用户登录权限校验1.1 最初用户登录验证1.2 Spring AOP 统一用户登录验证的问题1.3 SpringAOP 拦截器1.3.1 实现自定义拦截器1.3.2 将自定义拦截器加入到系统配置 1.4 拦截器实现原理1.4.1 实现流程图1.4.2 实现源码剖析 1.5 统一访问前缀添加 2. 统一异常处理2.1 创建一个异常处理类2.2 创建异常检测的类和异常处理方法 3. 统一数据返回3.1为什么需要统一数据返回?3.2 统一数据返回格式的实现3.3 统一异常处理在遇到 String 返回类型时报错的问题3.4 ControllerAdvice 源码剖析 1. 用户登录权限校验
1.1 最初用户登录验证
RestController
RequestMapping(/user)
public class UserController {
/**
* 某⽅法 1
*/
RequestMapping(/m1)
public Object method(HttpServletRequest request) {// 有 session 就获取没有不会创建HttpSession session request.getSession(false);if (session ! null session.getAttribute(userinfo) ! null) {// 说明已经登录业务处理return true;} else {// 未登录return false;}
}
/**
* 某⽅法 2
*/
RequestMapping(/m2)
public Object method2(HttpServletRequest request) {// 有 session 就获取没有不会创建HttpSession session request.getSession(false);if (session ! null session.getAttribute(userinfo) ! null) {// 说明已经登录业务处理return true;} else {// 未登录return false;}
}
// 其他⽅法...
}从上述代码中可以看出每个方法都相同的登录权限校验 , 这样做的缺点是:
每个方法中都要单独写用户登录验证的方法 , 即使封装成公共方法 , 也一样要在方法中传参判断.添加控制器越多, 调用用户登录的方法也越多 , 这样后期会增大维护成本.用户登录方法与接下来的业务实现没有任何关联 , 但还是要每个方法中写一遍.
因此, 使用 AOP 思想, 进行统一用户登录验证迫在眉睫. 1.2 Spring AOP 统一用户登录验证的问题
一说到用户登录验证 , 第一个想到的方法就是 , Spring AOP 前置或环绕通知来实现 , 具体实现代码如下:
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;}
}但是在 Spring AOP 的切面中实现用户登录校验有以下两个缺点:
没法获取到 HttpSession 对象由于需要拦截一部分方法 , 另一部分是不拦截的 , 如注册和登录方法不拦截 , 这样的话排除规则将无法定义. 1.3 SpringAOP 拦截器
Spring 中提供了具体的实现拦截器 HandlerInterceptor , 拦截器的实现分为以下两个步骤:
创建自定义拦截器 , 实现 HandlerInterceptor 接口的 preHandle(执行具体方法之前的预处理) 方法.将自定义拦截器加入 WebMvcConfigurer 的 addInterceptors 方法中.
具体实现如下:
1.3.1 实现自定义拦截器
//定义拦截器
Component
public class LoginInterceptor implements HandlerInterceptor {
// 调用目标方法之前执行的方法
// 此方法返回 boolean 类型的值 , 如果返回值为 true, 继续执行剩余流程, 否则表示拦截器验证未通过, 剩余的不在执行Overridepublic 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;}//如果执行失败不能直接给前端返回一个状态码, 后端必须明确告诉前端异常信息, 但状态码必须是200, //原理类似于确认应答, 如果是异常状态码前端无法接收到信息.response.setContentType(application/json;charsetutf8);response.getWriter().println({\code\:-1, \msg\:\登录失败\, \data\:\\});return false;}
}1.3.2 将自定义拦截器加入到系统配置
Configuration
public class MyConfig implements WebMvcConfigurer {Autowiredprivate LoginInterceptor loginInterceptor;Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(loginInterceptor).addPathPatterns(/**).excludePathPatterns(/user/login)//排除登录.excludePathPatterns(/user/reg);//排除注册}
}其中:
addPathPatterns() 表示需要拦截的 URLexcludePathPatterns() 表示需要排除的 URL
1.4 拦截器实现原理
1.4.1 实现流程图
Spring 项目中 , 正常的程序调用如下: 然而有了拦截器之后 , 就会在 Controller 之间进行预处理操作: 1.4.2 实现源码剖析
通过观察 Spring Boot 控制台的打印信息可知 , 所有的 Controller 执行都会通过一个调度器 DispatcherServlet 来实现. 所有方法都会执行 DispatcherServlet 中的 doDispatch 调度方法 , doDispatch 源码如下: 通过源码可以看出 , 执行 Controller 之前, 会先调用预处理方法 applyPreHandle() , applyPreHandle() 源码如下:
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {for(int i 0; i this.interceptorList.size(); this.interceptorIndex i) {//获取所有拦截器, 并调用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 并执行其中的 preHandle()方法 , 由此就与上文中的拦截器定义相对应. 通过上述源码分析 , 拦截器也是通过动态代理和环绕通知是思想实现的 , 大体流程如下: 1.5 统一访问前缀添加
在企业开发中 , 如果我们的项目工程较大且多个项目部署到同一台服务器上 , 如果不给具体的项目添加前缀 , 那么就会极大的增加维护成本.
eg. 给当前项目所有请求地址添加 api 前缀:
Configuaration
public class AppConfig implement WebMvcConfigurer(){Overridepublic void configurePathMatch(PathMatchConfigure configure){configure.addPathPrefix(api,c - true)}
}
第二个参数为表达式 , 设置 true 表示启动前缀.
那么后续访问时 , URL 都需要加上 api 前缀. 2. 统一异常处理
统一异常处理使用的是 ControllerAdvice ExceptionHandler 来实现的 , ControllerAdvice 表示控制器通知类 , ExceptionHandler 表示异常处理器 , 两个结合表示出现异常时执行某个通知 , 也就是执行某个方法事件 , 具体实现代码如下:
无论后端执行结果如何 , 都会给前端返回一个明确的信息.
2.1 创建一个异常处理类
import java.util.HashMap;ControllerAdvice//针对 Controller 的增强方法, 会检测控制器的异常
public class MyExceptionAdvice{}2.2 创建异常检测的类和异常处理方法
import java.util.HashMap;ControllerAdvice//针对 Controller 的增强方法, 会检测控制器的异常
ResponseBody //返回非静态页面 (数据)
public class MyExceptionAdvice{ExceptionHandler(NullPointerException.class)public HashMapString, Object doNullPointerException(NullPointerException e){HashMapString, 0bject result new HashMap();result.put(code, -1);result.put(msg, 空指针: e.getMessage());result.put(data, null);return result;}//默认异常处理, 当具体异常匹配不到时, 执行此方法ExceptionHandler(Exception.class)public HashMapString, Object doException(Exception e){HashMapString, 0bject result new HashMap();result.put(code, -300);result.put(msg, Exception: e.getMessage());result.put(data, null);return result;}
}3. 统一数据返回
3.1为什么需要统一数据返回?
方便前端程序员更好的接收和解析数据接口返回的数据降低前后端沟通成本有利于统一的数据维护和修改有利于后端技术部门统一标准的规定
保底策略 , 强制性统一数据返回 , 返回数据之前进行数据重写
3.2 统一数据返回格式的实现
统一返回数据的格式可以使用 ControllerAdvice ResponseBodeyAdvice 的方式实现 , 实现代码如下:
ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {//只有 true 时, 才会执行 beforeBodyWriter()Overridepublic boolean supports(MethodParameter returnType, Class converterType) {return true;}//返回数据之前对数据进行重写Overridepublic Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {//首先判断是否已经是标准格式了if (body instanceof HashMap){return body;}// 重写返回结果, 让其返回一个统一的数据格式HashMapString, Object result new HashMap();result.put(code, 200);result.put(msg, null);result.put(data, body);return result;}
}Tips: 实际开发中 , 通常不建议将 HashMap 作为返回类型 , 因为使用 HashMap 作为返回类型无法提供类型信息容易导致数据解析错误或类型转换异常 , 可读性差 , 维护困难. 3.3 统一异常处理在遇到 String 返回类型时报错的问题
当返回类型是 String 时
RequestMapping(/login)public String login(){return login;}控制台抛出异常: 如果剖析一下返回执行流程:
方法返回的是 String统一数据返回之前处理 ---- String 转换为 HashMap将 HashMap 转换为 application/json 字符串给前端
通过抓包可以看出 , 返回给前端的是 json 格式的数据 , 因此异常出现在第三步. 第三步转换时 , 首先查看原 Body 的数据类型:
是 String -- 调用 StringHttpMessageConverter 进行类型转换非 String -- 调用 HttpMessageConverter 进行类型转换
总而言之 , 原本是 HashMap 类型的数据 , 却被判断成 String 类型的数据 , 并调用 StringHttpMessageConverter 进行类型转换 , 于是就出现了 HashMap cannot be cast to java.lang.String
解决方案:
通过修改配置文件将 StringHttpMessageConverter 这个转换器从项目中去除.在统一数据重写时 , 单独处理 String 类型 , 让其返回一个 String 字符串 , 而非 HashMap
解决方案一:
Configuration
public class MyConfig implements WebMvcConfigurer {/*** 移除 StringHttpMessageConverter()* param converters*/Overridepublic void configureMessageConverters(ListHttpMessageConverter? converters) {converters.removeIf(converter - converters instanceof StringHttpMessageConverter);}
}解决方案二:
Autowiredprivate ObjectMapper objectMapper;SneakyThrowsOverridepublic Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {//首先判断是否已经是标准格式了if (body instanceof HashMap){return body;}// 重写返回结果, 让其返回一个统一的数据格式HashMapString, 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 实现源码如下:
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 组件 , 而所有的组件初始化都会调用 InitializingBean 接口.
通过查询 InitializingBean , 可以发现其中 Spring MVC 实现子类是 RequestMappingHandlerAdapter , 里面有一个 afterPropertiesSet() 方法 , 表示所有参数设置完成之后执行的方法.
package org.springframework.beans.factory;public interface InitializingBean {void afterPropertiesSet() throws Exception;
}在 afterPropertiesSet() 中有一个 initControllerAdviceCache 方法, 此方法的源码如下: 分析可知 , 该方法会查找所有的 ControllerAdvice 类 , 这些类未被存入容器中 , 但发生某个时间时 , 会调用相应的 Advice 方法 , 比如返回数据前调用统一数据封装. gHandlerAdapter , 里面有一个 afterPropertiesSet() 方法 , 表示所有参数设置完成之后执行的方法.
package org.springframework.beans.factory;public interface InitializingBean {void afterPropertiesSet() throws Exception;
}在 afterPropertiesSet() 中有一个 initControllerAdviceCache 方法, 此方法的源码如下:
[外链图片转存中…(img-mo8rbC9p-1689386373868)]
分析可知 , 该方法会查找所有的 ControllerAdvice 类 , 这些类未被存入容器中 , 但发生某个时间时 , 会调用相应的 Advice 方法 , 比如返回数据前调用统一数据封装.