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