文章目录
AOP思想的体现主要分为两个方面:
- Spring AOP用于分离功能性需求和非功能性需求,使得开发人员可以集中处理某一个关注点,减少对业务代码的侵入,增强代码的可读性和可维护性.
- SpringBoot的统一功能处理模块.
一. 用户登录权限效验
1. 最初用户登录验证
我们最初对用户登录验证的实现是这样的:
packagecom.example.demo.controller;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.RestController;importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpSession;@RestController@RequestMapping("/user")publicclassUserController{//执行的方法1@RequestMapping("/m1")publicObjectmethod1(HttpServletRequest request){//有 session 就创建,没有 session 就不会创建HttpSession session = request.getSession(false);if(session !=null&& session.getAttribute("userinfo")!=null){//说明已经登录,执行业务处理returntrue;}else{//未登录returnfalse;}}//执行的方法2@RequestMapping("/m1")publicObjectmethod2(HttpServletRequest request){//有 session 就创建,没有 session 就不会创建HttpSession session = request.getSession(false);if(session !=null&& session.getAttribute("userinfo")!=null){//说明已经登录,执行业务处理returntrue;}else{//未登录returnfalse;}}//其他需要执行的方法...}
从上述代码可以看出,每个方法的执行都有用户登录验证权限,它的缺点如下:
- 每个方法中都单独写用户登录验证的方法,即使封装成公共方法,也是一样要传参调用和在方法中进行判断.
- 添加控制器越多,调用用户登录验证的方法就越多,这样就增加了后期的修改成本和维护成本.
- 这些用户登录验证的方法和下面要执行的业务代码没有什么关系,但是在每个方法中都实现了一遍.
所以接下来我们提供一个公共的AOP方法来进行统一的用户登录权限验证.
2. Spring AOP 用户统一登录的验证
使用SpringAOP的具体实现代码如下:
packagecom.example.demo.common;importorg.aspectj.lang.ProceedingJoinPoint;importorg.aspectj.lang.annotation.Around;importorg.aspectj.lang.annotation.Aspect;importorg.aspectj.lang.annotation.Before;importorg.aspectj.lang.annotation.Pointcut;importorg.springframework.stereotype.Component;@Aspect@ComponentpublicclassUserAspect{// 定义切点⽅法 controller 包下、⼦孙包下所有类的所有⽅法@Pointcut("execution(* com.example.demo.controller..*.*(..))")publicvoidpointcut(){}// 前置⽅法@Before("pointcut()")publicvoiddoBefore(){}// 环绕⽅法@Around("pointcut()")publicObjectdoAround(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
对象。 - 我们要对一部分方法进行拦截,而另一部分方法不拦截,如注册方法和登录方法是不拦截的,这样的话排除方法的规则很难定义,甚至没办法定义。
那么我们应该如何解决呢?
3. Spring拦截器
针对以上问题,Spring中提供了具体的实现拦截器:
HandlerInterceptor
.拦截器的实现分为两个部分:
- 创建自定义拦截器,实现
HandlerInterceptor
接口的preHandle
(执行具体方法之前的预处理)方法. - 将自定义拦截器加入
WebMvcConfigurer
的addInterceptors
方法中.
具体实现如下:
3.1 自定义拦截器
实现一个
UserInterceptor
用户拦截器类,在该类中实现
HandlerInterceptor
接口,再重写
preHandle
方法
packagecom.example.demo.interceptor;importorg.springframework.stereotype.Component;importorg.springframework.web.servlet.HandlerInterceptor;importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletResponse;importjavax.servlet.http.HttpSession;@ComponentpublicclassUserInterceptorimplementsHandlerInterceptor{@OverridepublicbooleanpreHandle(HttpServletRequest request,HttpServletResponse response,Object handler)throwsException{//业务方法//从请求中取session,如果有session,直接获取到,但是没有,这里设置为false,也不会新创建一个session。HttpSession session = request.getSession(false);//这里添加false表示不会新创建session。方法中默认的是true。if(session!=null&& session.getAttribute("userinfo")!=null){returntrue;}
response.setStatus(401);//返回一个404returnfalse;}}
getAttribute
方法是
Object
类中的方法,用于获取对象的指定属性值,它接受一个参数,即要获取的属性的名称,并返回该属性的值,如果对象中不存在指定名称的属性,则返回
null
。该方法可以用于获取对象的任意属性,包括实例变量和静态变量。
3.2 将自定义拦截器设置到当前的项目中
实现一个
AppConfig
类用来配置,实现
WebMvcConfigurer
接口,然后重写其中的
addInterceptor
方法.
packagecom.example.demo.config;importcom.example.demo.interceptor.UserInterceptor;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.context.annotation.Configuration;importorg.springframework.web.servlet.config.annotation.InterceptorRegistry;importorg.springframework.web.servlet.config.annotation.WebMvcConfigurer;@ConfigurationpublicclassAppConfigimplementsWebMvcConfigurer{@AutowiredprivateUserInterceptor userInterceptor;@OverridepublicvoidaddInterceptors(InterceptorRegistry registry){
registry.addInterceptor(userInterceptor).addPathPatterns("/**")// 表示拦截所有的请求.excludePathPatterns("/user/reg")//排除不拦截的url.excludePathPatterns("/user/login")//排除不拦截的url;}}
这里的
addInterceptor
方法接受一个参数,就是要添加的拦截器对象。可以通过该方法添加一个或多个拦截器。
我们可以写一个
UserController1
测试类看一下运行结果:
packagecom.example.demo.controller;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.RestController;importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpSession;@RestController@RequestMapping("/user")publicclassUserController{@RequestMapping("/reg")publicStringreg(){return"do reg.";}@RequestMapping("/login")publicStringlogin(){return"do login.";}@RequestMapping("/test")publicStringtest(){return"do Test.";}}
由于我们的拦截器中拦截了所有的请求但是除了
/user/reg
/user/login
方法,所以这两个方法可以执行成功.而我们的Test方法则被拦截,返回
401
状态码.
当我们想要排除所有的静态文件,静态文件包含图片文件,前端的JS和CSS等文件,这个时候我们不可能将每种格式的文件都手动进行排除,这样工作量也太大了(图片文件存在几十种格式),想要将这些文件排除掉我们可以将这个静态文件放入项目的static目录中,然后针对这个目录中的子目录(图片,css文件,js文件)进行排除。
excludePathPatterns("/image/**")//表示排除image目录下的所有图片
4. 拦截器实现的原理
没有实现拦截器的时候,用户发送的请求直接被控制层接收到,进而在相应的URL中进行登录校验,这种方式代码的可维护性较低。
但是使用拦截器,用户发送的请求首先会被拦截器接收到,拦截器进行预处理,符合条件才会进一步调用Controller层的方法。
二. 统一的异常处理
我们之前处理异常的方法就是使用
try-catch
,或者是将异常抛出去给更上一层处理,这种方式处理异常的方式通常是分散在代码的各个部分中的,当应用程序出现异常时,开发需要在每个可能抛出异常的地方编写相应的异常处理代码,这样做会导致代码冗余,可读性差,并且难以维护。
1. 统一的异常处理优点
而使用统一的异常处理就可以:
- 集中处理异常:通过使用统一的异常处理机制,可以集中处理应用程序中的异常。这意味着无论在哪个控制器方法或服务方法中抛出异常,都可以在统一的地方进行处理,从而减少代码冗余。
- 统一错误响应:统一的异常处理机制可以确保应用程序返回一致的错误响应给客户端。这样做可以提高用户体验,让客户端能够更容易地理解和处理错误情况。
- 异常日志记录:通过统一的异常处理,可以方便地实现异常的日志记录。可以在异常处理器中添加日志记录的逻辑,记录异常的详细信息、发生时间和相关的上下文信息,以便后续的错误分析和故障排查。
- 异常转换和封装:统一的异常处理机制还可以进行异常的转换和封装。例如,可以将底层框架或第三方库的异常转换为应用程序定义的自定义异常,以简化异常的处理和管理。
- 统一的异常处理策略:通过统一的异常处理,可以定义全局的异常处理策略。可以根据不同的异常类型采取不同的处理方式,例如返回特定的错误码、跳转到指定的错误页面或执行其他自定义逻辑。
2. 统一的异常处理实现
在Spring Boot中,可以使用
@RestControllerAdvice
注解和
@ExceptionHandler
注解来实现统一异常处理。这两个注解搭配使用表示的是全局异常处理,可以捕获并处理全局范围内的异常。当控制器中抛出异常时,会根据异常类型匹配对应的
@ExceptionHandler
方法进行处理。
Exception
类是Java中所有异常类的父类。
@RestControllerAdvice
注解用在一个类上,表示该类是一个全局的控制器增强器,可以对所有的控制器进行统一的处理。这个注解提供了一种集中管理和统一处理全局范围内操作的方式,在引用程序中起到了很好的代码复用和统一管理的作用。@ExceptionHandler
注解,用于定义一个方法,该方法用于处理控制器中发生的异常。当控制器中的方法抛出异常时,@ExceptionHandler注解标记的方法将被调用来处理该异常。这样可以集中处理控制器中的异常。
- 创建一个异常处理类
- 创建异常检测的类和处理业务方法:
packagecom.example.demo;importorg.springframework.web.bind.annotation.ControllerAdvice;importorg.springframework.web.bind.annotation.ExceptionHandler;importorg.springframework.web.bind.annotation.ResponseBody;importjava.util.HashMap;@ControllerAdvice@ResponseBodypublicclassMyExpectionAdvice{@ExceptionHandler(NullPointerException.class)publicHashMap<String,Object>doNullPointerExpection(NullPointerException e){HashMap<String,Object> result =newHashMap<>();
result.put("code",-1);
result.put("msg","空指针"+e.getMessage());
result.put("data",null);return result;}}
写个
UserController
测试一下:
packagecom.example.demo.controller;importorg.springframework.stereotype.Controller;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.RestController;@RestController@RequestMapping("/user")publicclassUserController{@RequestMapping("/login")publicintlogin(){//空指针异常Object obj =null;System.out.println(obj.hashCode());return1;}}
此时我们就可以告诉前端异常的类型.上述代码我们处理了空指针异常,通常情况下,我们无法预测代码会抛出什么异常.所以我们可以使用所有异常的父类
Expection
来处理:
//默认的异常处理@ExceptionHandler(Exception.class)publicHashMap<String,Object>doException(Exception e){HashMap<String,Object> result =newHashMap<>();
result.put("code",-300);
result.put("msg","Exception:"+ e.getMessage());
result.put("data",null);return result;}
那么上述
doException
方法也可以处理空指针异常,当上述两个处理异常的方式同时存在时,首先采用的是
doNullPointerExpection
:(有子类先开始处理子类,再处理父类)
三. 统一数据返回格式
1. 统一数据返回格式的优点
- 方便前端程序员更好的接收和解析后端数据接口返回的数据。
- 降低前端程序员和后端程序员的沟通成本,按照某个个格式实现就行了,因为所有返回接口都是这样返回的
- 有利于项目统一数据的维护和修改
- 有利于后端技术部门的统一规范的标准指定,不会出现稀奇古怪的返回内容。
总结起来,统一数据返回格式可以提高接口的规范性、可读性和可维护性,方便异常处理,支持扩展和版本控制,并增强系统的兼容性。这些优点都有助于提高开发效率、减少错误和提升用户体验。
2. 统一数据返回格式的实现
统一的数据返回格式可以使用
@ControllerAdvice+ResponseBodyAdvice
的方式实现,实现步骤如下:
实现
ResponseBodyAdvice
接口.并重写其中的方法
supports
beforeBodyWrite
方法:
值得注意的是在此类中不需要加入
@ResponseBody
注解,这是因为在该类中只是对返回值进行转换.
packagecom.example.demo.controller;importorg.springframework.core.MethodParameter;importorg.springframework.http.MediaType;importorg.springframework.http.server.ServerHttpRequest;importorg.springframework.http.server.ServerHttpResponse;importorg.springframework.web.bind.annotation.ControllerAdvice;importorg.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;importjava.util.HashMap;@ControllerAdvicepublicclassResponseAdviceimplementsResponseBodyAdvice{//是否执行 beforeBodyWrite 方法,true=执行,重写返回结果@Overridepublicbooleansupports(MethodParameter returnType,Class converterType){returntrue;}//返回数据之前进行数据重写//@param body :原始返回值@OverridepublicObjectbeforeBodyWrite(Object body,MethodParameter returnType,MediaType selectedContentType,Class selectedConverterType,ServerHttpRequest request,ServerHttpResponse response){// HashMap<String,Object> -> code,msg,dataif(body instanceofHashMap){return body;}// 重写返回结果,让其返回一个统一的数据格式HashMap<String,Object> result =newHashMap<>();
result.put("code",200);
result.put("data", body);
result.put("msg","");return result;}}
写个测试方法测试一下:
@RequestMapping("/test")publicinttest(){return666;}
可以看到,上述结果返回的是我们指定的数据格式.
3. 统一移除处理在遇到String返回时报错问题
但是上述返回值有一个问题,即如果返回的类型是String类型时会报错:
测试方法:
@RequestMapping("/test1")publicStringtest1(){return"dotest1";}
运行结果:
要注意其返回的流程:
- 方法返回的是String
- 统一数据返回之前处理-> String Convert HashMap
- 将 HashMap 转换成 application/json 字符串给前端(接口)
显然,
Exception:java.util.HashMap cannot be cast to java.lang.String
是第三步出现了问题.第三步在执行的时候会判断
Body
的类型,如果是
String
类型,那么执行
StringHttpMessageConverter
进行类型转换;如果不是String类型,那么执行
HttpMessageConverter
进行类型转换.问题就出在了将
HashMap
转换成
application/json
字符串给前端(接口).针对以上问题,有两种解决方式:
- 将
StringHttpMessageConverter
去掉:
importorg.springframework.context.annotation.Configuration;importorg.springframework.http.converter.HttpMessageConverter;importorg.springframework.http.converter.StringHttpMessageConverter;importorg.springframework.web.servlet.config.annotation.WebMvcConfigurer;importjava.util.List;@ConfigurationpublicclassMyConfigimplementsWebMvcConfigurer{//移除 StringHttpMessageConverter@OverridepublicvoidconfigureMessageConverters(List<HttpMessageConverter<?>> converters){
converters.removeIf(converter -> converter instanceofStringHttpMessageConverter);}}
- 在统一数据重写时,单独处理
String
类型,让其返回一个String
字符串,而非HashMap
@OverridepublicObjectbeforeBodyWrite(Object body,MethodParameter returnType,MediaType selectedContentType,Class selectedConverterType,ServerHttpRequest request,ServerHttpResponse response){// HashMap<String,Object> -> code,msg,dataif(body instanceofHashMap){return body;}// 重写返回结果,让其返回一个统一的数据格式HashMap<String,Object> result =newHashMap<>();
result.put("code",200);
result.put("data", body);
result.put("msg","");if(body instanceofString){// 返回一个 将对象转换成 JSON String 字符串try{return objectMapper.writeValueAsString(result);}catch(JsonProcessingException e){
e.printStackTrace();}}return result;}
注意在上述代码中使用
import com.fasterxml.jackson.databind.ObjectMapper
包下的
objectMapper
方法需要引入Maven-jackson依赖:
<dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId></dependency>
小结
自定义拦截器的实现:使用
HandlerInterceptor
接口+
WebMvcConfigurer
接口实现。
统一异常的处理:使用
@RestControllerAdvice
注解+
@ExceptionHandler
注解实现。
统一数据返回格式:使用
@ControllerAdvice
注解+
ResponseBodyAdvice
接口实现。
版权归原作者 xxxflower. 所有, 如有侵权,请联系我们删除。