SpringBoot统一功能处理

关于SpringBoot统一处理。我们要实现的目标有三个:

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

1. 用户登录权限校验

1.1 最初用户登录验证

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

    /**
     * 某⽅法 1
     */
    @RequestMapping("/m1")
    public Object method(HttpServletRequest request) {
        return processRequest(request);
    }

    /**
     * 某⽅法 2
     */
    @RequestMapping("/m2")
    public Object method2(HttpServletRequest request) {
        return processRequest(request);
    }

    // Common logic for method and method2
    private Object processRequest(HttpServletRequest request) {
        // 有 session 就获取,没有不会创建
        HttpSession session = request.getSession(false);

        if (session != null && session.getAttribute("userinfo") != null) {
            // 说明已经登录,业务处理
            return true;
        } else {
            // 未登录
            return false;
        }
    }

    // 其他⽅法...
}

从上述代码可以看出来,每一个方法中都有相同的用户登录验证权限,它的缺点就是:

  1. 每一个方法中都要单独写用户登陆验证的方法,即使封装成公共方法,也一样要传参调用和在方法中进行判断
  2. 添加的控制器越多,调用用户的方法也越多,这样就会增加后期修改成本和维护成本
  3. 这些用户登录验证的方法合计下来掩饰显得业务几乎没有任何关联,但是每一个都要写一遍

所以提供一个公共的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() {
        // Your code for before advice
    }

    // 环绕⽅法
    @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 的切⾯中实现⽤户登录权限效验的功能,有以下两个问题:

  1. 没办法获取到 HttpSession 对象。
  2. 我们要对⼀部分⽅法进⾏拦截,⽽另⼀部分⽅法不拦截,如注册⽅法和登录⽅法是不拦截的,这样 的话排除⽅法的规则很难定义,甚⾄没办法定义。

如果我们需要解决这样的问题,我们就需要用到一种新的方法Spring拦截器

1.3 Spring拦截器

Spring提供了具体的实现拦截器:HandlerInterceptor,拦截器的实现分为以下两步:

  1. 创建自定义拦截器实现Heandlerinterceptor接口的prehanle(执行具体方法之前的预处理)方法
  2. 将自定义拦截器加入WebMvcConfigurer的addInterceptors方法中

1.3.1 自定义拦截器

接下来使⽤代码来实现⼀个⽤户登录的权限效验,⾃定义拦截器是⼀个普通类,具体实现代码如下:

public class UserInterceptor implements HandlerInterceptor {
    /**
     * 返回 true代表拦截器验证成功,继续执行后续方法
     *     false代表拦截器验证失败,不会执行后续目标方法
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
                             Object handler) throws Exception {
        //业务方法
        HttpSession session = request.getSession(false);
        if(session!=null && session.getAttribute(AppVar.SESSION_KEY)!=null){
            //用户已经登陆
            return true;
        }
        return false;
    }
}

 1.3.2 将自定义拦截器加入到系统配置

将上⼀步中的⾃定义拦截器加⼊到系统配置信息中,具体实现代码如下:

@Configuration
public class AppConfig implements WebMvcConfigurer {
    @Autowired
    public UserInterceptor userInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(userInterceptor)
                .addPathPatterns("/**")//拦截所有请求
                .excludePathPatterns("/login.html")
                .excludePathPatterns("/reg.html")
                .excludePathPatterns("/css/**")
                .excludePathPatterns("editor.md/**")
                .excludePathPatterns("/image/**")
                .excludePathPatterns("/user/reg")
                .excludePathPatterns("/user/getnum")
        ;
    }
}

addPathPatterns:表示需要拦截的URL,“**”表示拦截任意方法

excludePathPatterns:表示需要排除的URL

此时我们调用方法试一下:

SpringBoot统一功能处理_第1张图片

这里被成功拦截,我们在执行拦截功能的时候可以打印一下日志发现:

成功拦截 

当我们访问login页面便可以正常访问

2. 统一异常处理

统一异常处理使用的是@ControllerrAdice+@ExceptionHandler来实现的@ControllertAdvice表示控制器通知类,@ExceptionHandler是异常处理器,两个结合表示出当前出现异常的时候执行某个通知,也就是执行某个方法事件

@RestControllerAdvice
public class ExceptionAdvice {
   @ExceptionHandler(NullPointerException.class)
    public ResultAjax doNullPointerException(NullPointerException e){
       ResultAjax resultAjax = new ResultAjax();
       resultAjax.setCode(-1);
       resultAjax.setMsg("空指针异常:"+e.getMessage());
       resultAjax.setData(null);
       return resultAjax;
   }
   @ExceptionHandler(Exception.class)
   public ResultAjax doException(Exception e){
      ResultAjax resultAjax = new ResultAjax();
      resultAjax.setCode(-1);
      resultAjax.setMsg("异常:"+e.getMessage());
      resultAjax.setData(null);
      return resultAjax;
   }

}

SpringBoot统一功能处理_第2张图片当我们执行此代码的时候就会给前端页面返回一个异常

SpringBoot统一功能处理_第3张图片

 3. 统一数据返回格式

3.1 为什么需要统一数据返回格式

统⼀数据返回格式的优点有很多,⽐如以下⼏个:

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

3.2 统一数据返回格式的实现

@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
    /**
     * true才会调用beforeBodyWrite
     * @param returnType
     * @param converterType
     * @return
     */
    @Autowired
    private ObjectMapper objectMapper;
    @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) {
        if(body instanceof ResultAjax){
            return body;
        }
        if(body instanceof String){
            ResultAjax resultAjax = ResultAjax.succ(body);
            try {
                return objectMapper.writer().writeValueAsString(resultAjax);
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }
        }
        return ResultAjax.succ(body);
    }
}

返回对象:

package com.example.demo.common;

import com.sun.prism.PresentableState;
import lombok.Data;

@Data
public class ResultAjax {
    private int code; //状态码
    private String msg; //状态码描述信息
    private Object data; //返回数据
    /**
     * 返回成功
     */
    public static ResultAjax succ(Object data){
        ResultAjax resultAjax = new ResultAjax();
        resultAjax.setCode(200);
        resultAjax.setMsg("");
        resultAjax.setData(data);
        return resultAjax;
    }
    public static ResultAjax succ(String msg,Object data){
        ResultAjax resultAjax = new ResultAjax();
        resultAjax.setCode(200);
        resultAjax.setMsg(msg);
        resultAjax.setData(data);
        return resultAjax;
    }
    public static ResultAjax fail(int code,String msg){
        ResultAjax resultAjax = new ResultAjax();
        resultAjax.setCode(code);
        resultAjax.setMsg(msg);
        resultAjax.setData(null);
        return resultAjax;
    }
    public static ResultAjax fail(int code,String msg,Object data){
        ResultAjax resultAjax = new ResultAjax();
        resultAjax.setCode(code);
        resultAjax.setMsg(msg);
        resultAjax.setData(data);
        return resultAjax;
    }
}

有些人会好奇问什么需要单独判断String呢

SpringBoot统一功能处理_第4张图片

我们可以给前端返回一个String验证一下

SpringBoot统一功能处理_第5张图片

报错了

SpringBoot统一功能处理_第6张图片

为什么呢?是因为我们执行到这里的时候string渲染引擎没有加载完成,我们直接要返回一个json对象,就会报错

你可能感兴趣的:(spring,boot,java,后端)