Spring Boot 统一功能处理

统一功能处理

  • 作用
    • 用户登陆权限验证
    • 最初的用户登录验证
    • Spring AOP 统一登录验证
    • Spring 拦截器
      • 把拦截器加入到框架的配置中
      • 拦截器影响
    • 拦截器原理分析
    • 统一添加访问前缀
    • 统一异常处理
      • 算术异常
      • 空指针异常
      • 类型转换异常
      • 多异常处理
    • 统一数据返回格式

作用

统一功能处理的作用,也就是 AOP 的主要作用,主要如下:

  1. 统一用户登录权限验证
  2. 统一数据格式返回
  3. 统一异常处理

用户登陆权限验证

如果不使用 AOP 的话,

  1. 最初的用户登录权限校验要在每个方法里面获取 session 和 session 中的用户信息,如果用户信息存在,那么就登录成功了,否则就登录失败了。
  2. 第二版用户登录效验:提供了统一的方法,在每个需要验证的方法中调用统一的用户登录身份效验方法来判断。
  3. 第三版用户登录效验: 使用 Spring AOP 来使用统一的用户登录效验。
    就是说:不再需要我们敲代码去调用统一方法了,Spring AOP 会帮我们自动调用。但是也有问题:
    a)没有办法得到 HttpSession 和 Request 对象。
    b)实际拦截规则很复杂,使用简单 aspect j 表达式无法满足拦截的需求。
  4. 第四版登录校验:使用 Spring 拦截器来实现用户的统一登陆验证:
    a)实现自定义拦截器,主要就是实现 Spring 当中的 HandlerInterceptor 接口中的 preHandle 方法。
    b)将自定义拦截器加入到框架的设置中,并且设置拦截规则。

最初的用户登录验证

最初用户登录验证,要在每个方法类名获取 session 和 session 当中的信息:

@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;
        }
    }
// 其他⽅法...
}

像这样每个方法都有相同的用户登录验证权限,所有就导致很繁琐,而且添加的控制器越多,调用用户验证的方法也就越多,这样就增加了后期的修改成本和维护成本。

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;
    }
}

但是这样的话,没办法获取到 Session 对象,而且如果要对一部分进行拦截,一部分不拦截的话,是很难做到的。所以就通过 Spring 拦截器来实现。

Spring 拦截器

Spring 拦截器是基于 AOP 实现的,因为原生的 AOP 没有办法获取 HttpSession 和 Request 对象,而且拦截规则也很难定义。而 拦截器 当中,封装了很多对象,对象里面专门提供了专门的方法来解决这样的问题。

重写 HandlerInterceptor 接口的 preHandle 方法 :preHandle 返回是 true,则表示通过了 拦截器的验证,可以继续 执行,调用 目标方法了。反之,验证没有通过,直接返回一个错误信息。

使用拦截器之后 :就是前端先访问 拦截器,执行里面的 preHandle 方法,如果方法返回 true,则继续执行后面的代码。如果返回的是 false,直接返回一个错误信息,后面的代码就不执行了。示例代码如下:

@Component
public class LoginIntercept implements HandlerInterceptor {

    /**
     * 返回 true 表示拦截判断通过,可以访问后面的接口
     * 返回 false 表示拦截判断未通过,直接返回后面结果给前端
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //得到 HttpSession 对象
        HttpSession session = request.getSession(false);
        if (session != null && session.getAttribute("userinfo") != null) {
            //表示已经登录
            return true;
        }
        //如果未登录,然后就跳转到登陆页面
        response.sendRedirect("/login.html");
        return false;
    }
}

把拦截器加入到框架的配置中

把上一步的自定义拦截器加入到框架配置中,代码如下:

@Configuration
public class AppConfig implements WebMvcConfigurer {

    @Autowired
    private LoginIntercept loginIntercept;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginIntercept).addPathPatterns("/**").//拦截所有的 url
                excludePathPatterns("/user/login").//不拦截登录接口
                excludePathPatterns("/user/reg").//不拦截注册接口
                excludePathPatterns("/login.html").
                excludePathPatterns("/reg.html").
                excludePathPatterns("/**/*.js").
                excludePathPatterns("/**/*.css").
                excludePathPatterns("/**/*.png");
    }

    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        configurer.addPathPrefix("api", c -> true);//对所有的地址都要加上 api 前缀才能访问
    }
}

因为默认的配置文件都是叫做 Spring MVC Configurer 的一个文化。所以也要去实现 Web MVC Configurer 接口。然后在类上,添加 Configuration 注解,使其成系统的配置类。当前类实现 WebMvcConfigurer 接口。重写 WebMvcConfigurer 接口中的 addInterceptors 方法。就实现了 Spring 拦截器的流程。

访问 index 页面,运行结果如下:
Spring Boot 统一功能处理_第1张图片
因为我们代码当中设置了,如果未登陆的话,就跳转到登录界面:
Spring Boot 统一功能处理_第2张图片

设置 Session 然后再访问就可以访问到页面了。

拦截器影响

在不使用拦截器的时候,访问方式如下:
Spring Boot 统一功能处理_第3张图片
有了拦截器之后,在调用 Controller 之前会进行相应的业务处理,流程如下:
Spring Boot 统一功能处理_第4张图片

拦截器原理分析

从项目执行的日志来看,所有的 Controller 执行都会通过一个 调度器:DispatcherServlet 来实现:
Spring Boot 统一功能处理_第5张图片
每次执行的时候,都会去调用 DispatcherServlet ,因为 Dispatcher 的中文意思是 调度器。也就是说所有的请求来了之后,会先进入调度器,然后由调度器进行分发。就像我们买东西,快递都会先到当地的转运中心,然后才会分发,最后到我们手上。

拦截器也是通过 动态代理 和 环绕通知实现的,大概的调用流程如下:
Spring Boot 统一功能处理_第6张图片
然后,原本的 Spring AOP 的 切面类(代理对象) 换成了 DispatcherServlet 。一个是我们自定义的,一个 Spring 框架 自带的。不过执行原理并没有变,用户 想要与 目标对象 直接交互,必须要通过 代理对象(拦截器)的验证。通过之后,才可以访问目标对象。

统一添加访问前缀

也就是对所有请求地址添加 api 前缀:

@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
    configurer.addPathPrefix("api", c -> true);//对所有的地址都要加上 api 前缀才能访问
}

不加 api 的话,运行结果如下:
Spring Boot 统一功能处理_第7张图片

统一异常处理

对于异常的处理,按道理来说,我们是要给前端返回一个 JSON 格式的信息,但是如果不小心写了一个 bug :

@RequestMapping("/index")
public String index() {
    int num = 10 / 0;
    return "hello index";
}

也就是如过返回的话,前端会报 500 异常的。如果加上 try catch 也可以完成异常的捕获,但是就不能进行数据的回滚了。

实现统一异常处理有两步

  1. 给类上面加一个 @ControllerAdvice / @RestControllerAdvice 注解,相当于是对 controller 方法的增强。
  2. 给方法上加一个 @ExceptionHandler 注解,但是后面要带参数,表示要处理异常的类型。然后添加异常返回的业务代码就好了。

算术异常

因为是算数异常,所有就可以直接加算术异常:

@RestControllerAdvice
public class MyExceptionAdvice {

    @ExceptionHandler(ArithmeticException.class)
    public HashMap<String, Object> arithmeticExceptionAdvice(ArithmeticException e) {
        HashMap<String, Object> result = new HashMap<>();
        result.put("state", -1);
        result.put("data", null);
        result.put("msg", "算术异常:" + e.getMessage());
        return result;
    }
}

前端访问如下:
Spring Boot 统一功能处理_第8张图片
就不会直接报错了。

空指针异常

代码如下:

@RequestMapping("/index2")
public String index2() {
    Object obj = null;
    System.out.println(obj.hashCode());
    return "hello index2";
}

访问结果如下:
Spring Boot 统一功能处理_第9张图片
然后再捕获一次空指针异常:

@ExceptionHandler(NullPointerException.class)
public HashMap<String, Object> nullPointerExceptionAdvice(NullPointerException e) {
    HashMap<String, Object> result = new HashMap<>();
    result.put("state", -1);
    result.put("data", null);
    result.put("msg", "空指针异常:" + e.getMessage());
    return result;
}

运行结果如下:
Spring Boot 统一功能处理_第10张图片

类型转换异常

代码如下:

@RequestMapping("/index3")
public String index3() {
    String obj = "aaa";
    System.out.println(Integer.valueOf(obj));
    return "hello index2";
}

访问结果如下:
Spring Boot 统一功能处理_第11张图片

多异常处理

也就是异常很多很多,我们不能穷举,所以通过 所有异常的 父类来实现:

@ExceptionHandler(Exception.class)
public HashMap<String, Object> exceptionAdvice(Exception e) {
    HashMap<String, Object> result = new HashMap<>();
    result.put("state", -1);
    result.put("data", null);
    result.put("msg", "异常:" + e.getMessage());
    return result;
}

有了这样的异常处理之后,就不用像上面这样挨个写异常了。不过上面写的异常时优先级最高的。

统一数据返回格式

统一数据返回格式的优势是:

  1. ⽅便前端程序员更好的接收和解析后端数据接⼝返回的数据。
  2. 降低前端程序员和后端程序员的沟通成本,按照某个格式实现就⾏了,因为所有接⼝都是这样返回的。
  3. 有利于项⽬统⼀数据的维护和修改。
  4. 有利于后端技术部⻔的统⼀规范的标准制定,不会出现稀奇古怪的返回内容。

实现统一数据返回的时候,还是通过两个步骤:

  1. 添加 @ControllerAdvice 注解。
  2. 实现 ResponseBodyAdvice 接口,并且重写里面的方法。必须重写 里面的 supports 和 beforeBodyWrite 方法。

supports 方法返回一个 boolean 值。true 就表示返回数据之前对数据进行重写,也就是会进入 beforeBodyWrite 方法。如果返回 false 表示对结果不进行任何处理,直接返回:

@ControllerAdvice
public class MyResponseAdvice implements ResponseBodyAdvice {

    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        HashMap<String, Object> result = new HashMap<>();
        result.put("state", 1);
        result.put("data", body);
        result.put("msg", "");
        return result;
    }
}

然后我们通过 登录 和 注册 来查看是否属于统一格式:

@RestController
@RequestMapping("/user")
public class UserController {

    @RequestMapping("/login")
    public boolean login(HttpServletRequest request, String username, String password) {
        boolean result = false;
        if (StringUtils.hasLength(username) && StringUtils.hasLength(password)) {
            if (username.equals("admin") && password.equals("admin")) {
                HttpSession session = request.getSession();
                session.setAttribute("userinfo", "userinfo");
                return true;
            }
        }
        return result;
    }

    @RequestMapping( "/reg")
    public int reg() {
        return 1;
    }
}

通过 login 访问:
Spring Boot 统一功能处理_第12张图片
通过 reg 访问:
Spring Boot 统一功能处理_第13张图片
就可以发现,这样的话,返回的数据格式也完成了统一。

你可能感兴趣的:(JavaEE,spring,boot,spring,java,统一功能处理,统一处理)