Spring Boot(6)之 进阶

1、什么是AOP

1.1、对AOP的初印象

首先先给出一段比较专业的术语(来自百度):

在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方
式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个
热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑
的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高
了开发的效率。

然后我们举一个比较容易理解的例子(来自:Spring 之 AOP):

要理解切面编程,就需要先理解什么是切面。用刀把一个西瓜分成两瓣,切开的切口就是切面;炒菜,锅与炉子共同来完成炒菜,锅与炉子就是切面。web层级设计中,web层->网关层->服务层->数据层,每一层之间也是一个切面。编程中,对象与对象之间,方法与方法之间,模块与模块之间都是一个个切面。

我们一般做活动的时候,一般对每一个接口都会做活动的有效性校验(是否开始、是否结束等等)、以及这个接口是不是需要用户登录。按照正常的逻辑,我们可以这么做。
Spring Boot(6)之 进阶
这有个问题就是,有多少接口,就要多少次代码copy。对于一个“懒人”,这是不可容忍的。好,提出一个公共方法,每个接口都来调用这个接口。这里有点切面的味道了。
Spring Boot(6)之 进阶
同样有个问题,我虽然不用每次都copy代码了,但是,每个接口总得要调用这个方法吧。于是就有了切面的概念,我将方法注入到接口调用的某个地方(切点)。
Spring Boot(6)之 进阶

这样接口只需要关心具体的业务,而不需要关注其他非该接口关注的逻辑或处理。
红框处,就是面向切面编程。

1.2、AOP中的相关概念

看过了上面的例子,我想大家脑中对AOP已经有了一个大致的雏形,但是又对上面提到的切面之类的术语有一些模糊的地方,接下来就来讲解一下AOP中的相关概念,了解了AOP中的概念,才能真正的掌握AOP的精髓。

1.2.1、这里还是先给出一个比较专业的概念定义:

  • Aspect(切面)Aspect 声明类似于 Java 中的类声明,在 Aspect 中会包含着一些 Pointcut 以及相应的 Advice
  • Joint point(连接点):表示在程序中明确定义的点,典型的包括方法调用对类成员的访问以及异常处理程序块的执行等等,它自身还可以嵌套其它 joint point
  • Pointcut(切点):表示一组 joint point,这些 joint point 或是通过逻辑关系组合起来,或是通过通配正则表达式等方式集中起来,它定义了相应的 Advice 将要发生的地方。
  • Advice(增强)Advice 定义了在 Pointcut 里面定义的程序点具体要做的操作,它通过 beforeafteraround 来区别是在每个 joint point 之前、之后还是代替执行的代码。
  • Target(目标对象):织入 Advice目标对象
  • Weaving(织入):将 Aspect其他对象连接起来, 并创建 Adviced object 的过程

1.2.2、然后举一个容易理解的例子:

看完了上面的理论部分知识, 我相信还是会有不少朋友感觉到 AOP 的概念还是很模糊, 对 AOP 中的各种概念理解的还不是很透彻. 其实这很正常, 因为 AOP 中的概念实在是太多了, 我当时也是花了老大劲才梳理清楚的.

  • 下面我以一个简单的例子来比喻一下 AOPAspect, Joint point, PointcutAdvice之间的关系.

让我们来假设一下, 从前有一个叫爪哇的小县城, 在一个月黑风高的晚上, 这个县城中发生了命案. 作案的凶手十分狡猾, 现场没有留下什么有价值的线索. 不过万幸的是, 刚从隔壁回来的老王恰好在这时候无意中发现了凶手行凶的过程, 但是由于天色已晚, 加上凶手蒙着面, 老王并没有看清凶手的面目, 只知道凶手是个男性, 身高约七尺五寸. 爪哇县的县令根据老王的描述, 对守门的士兵下命令说: 凡是发现有身高七尺五寸的男性, 都要抓过来审问. 士兵当然不敢违背县令的命令, 只好把进出城的所有符合条件的人都抓了起来.

来让我们看一下上面的一个小故事和 AOP 到底有什么对应关系.
首先我们知道, 在 Spring AOPJoint point 指代的是所有方法的执行点, 而 point cut 是一个描述信息, 它修饰的是 Joint point, 通过 point cut, 我们就可以确定哪些 Joint point 可以被织入 Advice。 对应到我们在上面举的例子, 我们可以做一个简单的类比, Joint point 就相当于 爪哇的小县城里的百姓,pointcut 就相当于 老王所做的指控, 即凶手是个男性, 身高约七尺五寸, 而 Advice 则是施加在符合老王所描述的嫌疑人的动作: 抓过来审问.
为什么可以这样类比呢?

  • Joint point爪哇的小县城里的百姓: 因为根据定义, Joint point 是所有可能被织入 Advice 的候选的点, 在 Spring AOP中, 则可以认为所有方法执行点都是 Joint point. 而在我们上面的例子中, 命案发生在小县城中, 按理说在此县城中的所有人都有可能是嫌疑人。

  • Pointcut男性, 身高约七尺五寸: 我们知道, 所有的方法(joint point) 都可以织入 Advice, 但是我们并不希望在所有方法上都织入 Advice, 而 Pointcut 的作用就是提供一组规则来匹配joinpoint, 给满足规则的 joinpoint 添加 Advice。同理, 对于县令来说, 他再昏庸, 也知道不能把县城中的所有百姓都抓起来审问, 而是根据凶手是个男性, 身高约七尺五寸, 把符合条件的人抓起来. 在这里 凶手是个男性, 身高约七尺五寸 就是一个修饰谓语, 它限定了凶手的范围, 满足此修饰规则的百姓都是嫌疑人, 都需要抓起来审问。

  • Advice抓过来审问, Advice 是一个动作, 即一段 Java 代码, 这段 Java 代码是作用于 point cut 所限定的那些 Joint point 上的。同理, 对比到我们的例子中, 抓过来审问 这个动作就是对作用于那些满足 男性, 身高约七尺五寸 的爪哇的小县城里的百姓。

  • AspectAspectpoint cutAdvice 的组合, 因此在这里我们就可以类比: “根据老王的线索, 凡是发现有身高七尺五寸的男性, 都要抓过来审问” 这一整个动作可以被认为是一个 Aspect。

最后是一个描述这些概念之间关系的图:
Spring Boot(6)之 进阶

1.3、其他的一些内容

AOP中的Joinpoint可以有多种类型:构造方法调用字段的设置和获取方法的调用方法的执行异常的处理执行类的初始化。也就是说在AOP的概念中我们可以在上面的这些Joinpoint上织入我们自定义的Advice,但是在Spring中却没有实现上面所有的joinpoint,确切的说,Spring只支持方法执行类型的Joinpoint。

1.3.1、Advice 的类型

  • before advice, 在 join point 前被执行的 advice. 虽然 before advice 是在 join point 前被执行, 但是它并不能够阻止 join point 的执行, 除非发生了异常(即我们在 before advice 代码中, 不能人为地决定是否继续执行 join point 中的代码)
  • after return advice, 在一个 join point 正常返回后执行的 advice
  • after throwing advice, 当一个 join point 抛出异常后执行的 advice
  • after(final) advice, 无论一个 join point 是正常退出还是发生了异常, 都会被执行的 advice.
  • around advice, 在 join point 前和 joint point 退出后都执行的 advice. 这个是最常用的 advice.
  • introduction,introduction可以为原有的对象增加新的属性和方法。

1.4、Spring Boot添加AOP

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

1.5、用AOP方式管理日志

com/example/test3/aop/AopLog.java

package com.example.test3.aop;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

@Aspect // 使之成为切面类
@Component // 把切面类加入 loC 容器中
public class AopLog {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    // 线程局部的变量,用于解决多线程中相同变量的访问冲突
    ThreadLocal<Long> startTime = new ThreadLocal<>();

    // 定义切点,此处捕获 com.example 包下的所有方法,容易导致其他非请求类方法异常。
    @Pointcut("execution(public * com.example..*.*(..))")
    public void aopWebLog() {
    }

    // 在切入点前后切入内容,并控制何时执行切入点自身的内容
    @Around("aopWebLog()")
    public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
        //执行目标方法
        Object obj = pjp.proceed();
        //只有返回了obj,@AfterReturning才获取返回的结果,否则哪里的result为null
        return obj;
    }

    // 在切入点开始处切入内容
    @Before("aopWebLog()")
    public void doBefore(JoinPoint joinPoint) throws Throwable {
        startTime.set(System.currentTimeMillis());

        // 接收到请求,记录请求内容
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();

        // 记录下请求内容
        logger.info("URL: " + request.getRequestURI());
        logger.info("HTTP方法: " + request.getMethod());
        logger.info("IP地址: " + request.getRemoteAddr());
        logger.info("类的方法: " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
        logger.info("参数: " + request.getQueryString());
    }

    // 在切入点结尾处切入内容,排在 @AfterReturning 等之后执行
    @After("aopWebLog()")
    public void doAfter() {
    }

    // 在切入点返回(return)内容之后切入内容,可以用来处理返回值做一些加工处理
    @AfterReturning(pointcut = "aopWebLog()", returning = "retObject")
    public void doAfterReturning(Object retObject) throws Throwable {
        // 处理完请求,返回内容
        logger.info("应答值: " + retObject);
        logger.info("耗时: " + (System.currentTimeMillis() - startTime.get()));
    }

    // 用来处理当切入内容部分抛出异常之后的处理逻辑
    @AfterThrowing(pointcut = "aopWebLog()", throwing = "ex")
    public void addAfterThrowingLogger(JoinPoint joinPoint, Exception ex) {
        String classMethodName = joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName();
        logger.error("执行 " + classMethodName + " 异常", ex);
    }
}
  • 请求地址
// Controller,其他内容略
@CrossOrigin // 允许跨域访问
@RequestMapping("/getData")
@ResponseBody
public CompareResult generateData() {
    List<excelData> data = new ArrayList<>();
    for (int i = 0; i < 20; i++) {
        data.add(new excelData("AssistantScreen_" + i, "应用中文名称_" + i, true, "责任团队", "1.61.16", "1.53.6"));
    }
    return new CompareResult(0, "ok", data);
}

2、使用loC管理Bean

2.1、Spring loC简介

2.2、创建一个Bean

package com.example.test3.bean;

import lombok.Data;

import java.io.Serializable;

@Data
public class User implements Serializable {
    private Integer id;
    private String name;
}

2.3、编写User配置类

package com.example.test3.configuration;

import com.example.test3.bean.User;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration // 用于注释配置类,让Spring来加载该类配置作为Bean的载体。在运行时,将为这些Bean生成BeanDefinition和服务请求。
public class UserConfig {
    
    // Bean名称为方法默认值:user1
    @Bean // 产生一个Bean,并交给Spring管理。目的是封装用户,数据库中的数据,一般有Setter、Getter方法。
    public User user1(){
        User user = new User();
        user.setId(1);
        user.setName("张三");
        return user;
    }
    
    // Bean名称通过value指定了:userBean
    @Bean("userBean")
    public User user2(){
        User user = new User();
        user.setId(2);
        user.setName("赵四");
        return user;
    }

    // Bean名称为:userBean1,2个别名:[testBean,myBean]
    @Bean({"userBean1","testBean","myBean"})
    public User user3(){
        User user = new User();
        user.setId(3);
        user.setName("王五");
        return user;
    }
}

2.4、编写测试类

注意:测试类在测试包路径下书写:src/test/java/com/example/test3/configuration/UserConfigTest.java

package com.example.test3.configuration;

import com.example.test3.bean.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;

@SpringBootTest // 用于测试的注解,可指定入口类或测试环境等;@Autowired 注解必须要添加 @SpringBootTest
public class UserConfigTest {

    @Autowired
    private ApplicationContext applicationContext; // 注入,获取Spring容器中已初始化的Bean

    @Test // 测试方法
    public void testloC() {
        // 实例化User对象,通过上下文获取Bean对象user1
        User user = (User) applicationContext.getBean("user1");
        // 在控制台中打印User数据
        System.out.println(user);
    }
}

3、用 Servlet 处理请求

3.1、认识 Servlet

Servlet实在javax.servlet包中定义的一个接口。在开发SpringBoot程序中,使用Controller基本能解决大部分的功能需求。但有时也需要使用Servlet,比如实现拦截和监听功能。

SpringBoot的核心控制器DispatcherServlet会处理所有请求。如果自定义Servlet,则需要进行注册,以便DispatcherServlet核心控制器知道它的作用,以及处理请求url-pattern。

3.2、注册 Servlet 类

package com.example.test3.dome;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet(urlPatterns = "/ServletDemo/*") // 属性 urlPatterns 指定 WebServlet 的作用范围,这里代表 ServletDemo 下的所有子路径
public class ServletDemo extends HttpServlet {

    @Override // 父类 HttpServlet 的 doGet 方法是空的,所以需要重写
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("doGet");
        resp.getWriter().print("Servlet ServletDemo");
    }
}

3.3、开启 Servlet 支持

package com.example.test3.dome;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;

@ServletComponentScan // 在入口类上添加注解,使 Servlet 生效。组件扫描,可自动发现和装配一些Bean,并根据定义的扫描路径把符合扫描规则的类装配到 Spring 容器中。
@SpringBootApplication // 入口类 Application 的启动注解。
public class ServletDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(ServletDemoApplication.class, args);
    }
}

4、过滤器与监听器

在很多Web项目中,都会用到过滤器(Filter),如参数过滤、放置SQL注入、防止页面工具、空参数矫正、Token验证、Session验证、点击率统计等。

4.1、认识过滤器

4.1.1、为什么要使用过滤器

在Web开发中,常常会有这样的需求:在所有接口中去除用户输入的非法字符,以防止因其业务异常。要实现这个功能,可以有很多方法。如:

  • 在前端参数传入时进行校验,先过滤掉非法字符,然后,返回用户界面提示用户重新输入。
  • 后端接收前端没有过滤的数据,然后过滤非法字符。
  • 利用 Filter 处理项目中所有非法字符。
    很明显,前两种方法会存在重复代码。如果用过滤器来实现,则只需要用过滤器对所有接口进行过滤处理。这样非常方便,同时不会出现冗余代码。

4.2、使用 Filter 的步骤

  1. 新建类,实现 Filter 抽象类。
  2. 重写 init、doFilter、destroy方法。
  3. 在 Spring Boot 入口中添加注解 @ServletComponentScan,以注册Filter。

在重写3个方法后,还可以进一步修改 request 参数使用的封装方式,如:

  1. 编写 ParameterRequestWrqpper 类继承 HTTPServletRequestWrapper类。
  2. 编写 ParameterRequestWrqpper 类的构造器。
  3. 在构造器中覆盖父类构造器,并将 request.getParameterMap 加入子类的成员变量。
  4. 编写 addParam 方法。
  5. 修改参数并调用 ParameterRequestWrapper 实例,并保存 param。
  6. 调用 doFilter 方法中的 FilterChain 变量,以重新封装修改后的 request。

4.2.1、编写过滤器类

编写过滤器类,并通过注解 @Order 设置过滤器的执行顺序。序号越小,越早被执行。

package com.example.test3.filter;

import org.springframework.core.annotation.Order;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;

@Order(1) // 如果有多个 Filter ,则序号越小,越早被执行
@WebFilter(filterName = "FilterDemo", urlPatterns = "/*") // URL过滤配置
public class FilterDemo implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // 在服务器启动时被调用
        System.out.println("在服务器启动时被调用");
    }

    @Override
    public void destroy() {
        // 在服务器关闭时被调用
        System.out.println("在服务器关闭时被调用");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        // 请求 request 处理逻辑
        System.out.println("请求 request 处理逻辑");
        // 请求 request 封装逻辑
        System.out.println("请求 request 封装逻辑");
        // chain 重新写回 request 和 response
        System.out.println("chain 重新写回 request 和 response");
        // 返回消息,没有这一条,将无法正常返回消息
        filterChain.doFilter(servletRequest, servletResponse);
    }
}

4.2.2、在 Spring Boot 入口类注册 Filter

package com.example.test3;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;

@ServletComponentScan // 入口类添加 @ServletComponentScan 即可
@SpringBootApplication
public class Test3Application {

    public static void main(String[] args) {
        SpringApplication.run(Test3Application.class, args);
    }

}

4.3、认识监听器

监听器用于监听Web应用程序中某些对象或信息的创建、销毁、增加、修改、删除等操作,然后做出相应的相应处理。当对象的状态发生变化时,服务器自动调用监听器的方法,监听器常用于统计在线人数、在线用户、系统加载时的信息初始化等。

Servlet中的监听器分为以下3种类型:
Spring Boot(6)之 进阶

4.4、实现监听器

4.4.1、创建监听类

package com.example.test3.listener;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;

@WebListener
public class ListenerDemo implements ServletContextListener {

    @Override
    public void contextInitialized(ServletContextEvent sce) {
        System.out.println("ServletContext 初始化");
        System.out.println(sce.getServletContext().getServerInfo());
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        System.out.println("ServletContext 销毁");
    }

}

4.4.2、开启监听器 Bean 扫描

在入口类上,添加注解 @ServletComponentScan

package com.example.test3;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;

@ServletComponentScan // 入口类添加 @ServletComponentScan 即可
@SpringBootApplication
public class Test3Application {

    public static void main(String[] args) {
        SpringApplication.run(Test3Application.class, args);
    }

}

5、元注解

5.1、了解元注解

Java除了内置了三种标准注解,还有四种元注解。

注解 说明
@Target 表明该注解使用范围
@Retention 表明该注解的生命周期
@Documented 表明该注解是由Javadoc记录的,及是否将注解信息添加到生成的Java文档中
@Inherited 表明该注解可以被子类继承
@interface 用来声明自定义注释

5.1.1、@Target

表示该注解用于什么地方,可能的值在枚举类 ElemenetType 中,包括:

 ElemenetType.CONSTRUCTOR    -------------------------构造器声明 
 ElemenetType.FIELD          -------------------------域声明(包括 enum 实例) 
 ElemenetType.LOCAL_VARIABLE ------------------------ 局部变量声明 
 ElemenetType.METHOD         -------------------------方法声明 
 ElemenetType.PACKAGE        -------------------------包声明 
 ElemenetType.PARAMETER      -------------------------参数声明 
 ElemenetType.TYPE           ------------------------ 类,接口(包括注解类型)或enum声明 

5.1.2、@Retention

表示在什么级别保存该注解信息。可选的参数值在枚举类型 RetentionPolicy 中,包括:

 RetentionPolicy.SOURCE  -----------注解将被编译器丢弃 
 RetentionPolicy.CLASS   -----------注解在class文件中可用,但会被VM丢弃 
 RetentionPolicy.RUNTIME -----------VM将在运行期也保留注释,因此可以通过反射机制读取注解的信息。

5.1.3、@Documented

将此注解包含在 javadoc 中 ,它代表着此注解会被javadoc工具提取成文档。在doc文档中的内容会因为此注解的信息内容不同而不同。相当与@see,@param 等。

5.1.4、@Inherited

允许子类继承父类中的注解。

5.1.5、@interface

该注解用来声明一个注解,其中的每一个方法实际上是声明了一个配置参数。方法名称就是参数的名称,返回值类型就是参数类型(返回值类型只能是基础类型、Class、String、enum)。可以通过 default 来声明参数的默认值。

定义注解格式:

public @interface 注解名 {定义体}

5.2、自定义注解

5.2.1、创建自定义注解类

package com.example.test3.bean;

import java.lang.annotation.*;

@Target({ElementType.FIELD, ElementType.METHOD}) // 注解作用范围
@Retention(RetentionPolicy.RUNTIME) // 注解生命周期
@Documented // 注解信息添加到 Java 文档中
public @interface Fields {
    String value() default "";
}

5.2.2、AOP注入检测被注解的方法

package com.example.test3.aop;

import com.example.test3.bean.Fields;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

/**
 * 捕获被 @Fields 注解的方法,打印注解赋予的值。
 */
@Aspect
@Component
public class AopAnnotation {

    // 切点
    @Pointcut("@annotation(com.example.test3.bean.Fields)")
    public void aopAnnotation() {
    }

    // 切入点之前打印注解值
    @Before("aopAnnotation()")
    public void doBefore(JoinPoint joinPoint) throws Throwable {
        MethodSignature sign = (MethodSignature) joinPoint.getSignature();
        Method method = sign.getMethod();
        Fields annotation = method.getAnnotation(Fields.class);

        System.out.println("Fields 注解值:" + annotation.value());
    }
}

5.2.3、使用注解

package com.example.test3.controller;

import com.example.test3.bean.CompareResult;
import com.example.test3.bean.Fields;
import com.example.test3.bean.excelData;
import com.example.test3.excel.ExcelExport;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

@Controller
public class TestExcel {

    @CrossOrigin
    @Fields("get请求:获取数据")
    @RequestMapping("/getData")
    @ResponseBody
    public CompareResult generateData() {
        List<excelData> data = new ArrayList<>();
        for (int i = 0; i < 20; i++) {
            data.add(new excelData("AssistantScreen_" + i, "应用中文名称_" + i, true, "责任团队", "1.61.16", "1.53.6"));
        }
        return new CompareResult(0, "ok", data);
    }

    @CrossOrigin
    @Fields("get请求:下载数据Excel")
    @RequestMapping("/downloadExcel")
    public void downloadExcel(HttpServletResponse response, @RequestBody CompareResult data) {
        try {
            ExcelExport.exportExcel(response, "测试文件", "Sheet", data.getData());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

6、异常处理

6.1、捕获异常处理

try {
    // 尝试执行可能出现异常的代码
}
catch {
    // 捕获异常并处理
}
finally {
    // 无论是否异常,都会被执行
}

6.2、抛出异常

  • 手动抛出异常:throw (异常对象);
  • 方法抛出异常:[(修饰符)](返回值)(方法名)([参数列表])[throws(异常类)]{...}

6.3、使用控制器通知

在编写代码时,需要对异常进行处理。进行异常处理的普通代码是 try…catch 结构。但是在开发中,只想关注业务正常代码,对于 catch 语句中的异常捕获,希望交给异常捕获来处理,不单独在每个方法中编写。这样不仅可以减少冗余代码,还可以减少因忘记写 catch 而出现错误的概率。

Spring 正好提供了一个非常方便的异常处理方案----控制器通知(@ControllerAdvice@RestcontrollerAdvice),它将所有控制器作为一个切面,利用切面技术来实现。

通过基于 @ControllerAdvice@RestcontrollerAdvice 的注解可以对异常进行全局统一处理,默认对全部 Controller 有效。如果要限定生效范围,则可以使用 @ControllerAdvice 支持的方式:

  • 按注解:@ControllerAdvice(annotation = RestController.class)
  • 按包名:@ControllerAdvice(“com.example.test3.controller”)
  • 按类型:@ControllerAdvice(annotationTypes = {ControllerInterface.class, AbstractController.class})

与Spring AOP一样,Spring MVC也能够给控制器加入通知,它主要涉及4个注解:

  • @ControllerAdvice:主要作用于统一异常处理,通过@ExceptionHandler(value = Exception.class)来指定捕获的异常。“@ControllerAdvice + @ExceptionHandler”可以处理除“404”以外的运行异常。
  • @InitBinder:对表单数据进行绑定,用于定义控制器参数绑定规则。如转换规则、格式化等。允许构建POJO参数的方法,允许在构造控制器参数的时候,加入一定的自定义控制。可以通过这个注解的方法得到 WebDataBinder 对象,它在参数转换之前被执行。
  • @ExceptionHandler:定义控制器发生异常后的操作,使用当控制器发生注册异常时,就会跳转到该方法上,可以拦截所有控制器发生的异常
  • @ModelAttribute:是一种针对于数据模型的注解,它先于控制器方法运行,对所有 Controller 的 Model 添加数据进行操作,当标注方法返回对象时,它会保存到数据模型中。

6.4、自定义错误处理控制器

6.4.1、自定义一个错误的处理控制器

package com.example.test3.controller;

import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;


@RestController
@RequestMapping("/error") // 直接代替error页面
//@RequestMapping("${server.error.path:${error.path:/error}}") // 这种方式也可以
public class ErrorControllerNew implements ErrorController {

    // 这个必须要重写,否则会报错,默认返回 null 就可以
    @Override
    public String getErrorPath() {
        return null;
    }

    @RequestMapping
    public Map<String, Object> handleError() {
        // 用Map容器返回信息
        Map<String, Object> map = new HashMap<>();
        map.put("code", 404);
        map.put("msg", "不存在");
        return map;
    }

}

6.4.2、根据请求返回相应的数据格式

package com.example.test3.controller;

import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;


@RestController
@RequestMapping("/error")
//@RequestMapping("${server.error.path:${error.path:/error}}")
public class ErrorControllerNew implements ErrorController {

    private final String requestTypeText = "text/html;charset=UTF-8";
    private final String requestTypeJson = "application/json;charset=UTF-8";

    @Override
    public String getErrorPath() {
        return null;
    }

    @RequestMapping(produces = requestTypeText)
    public String errorHtml404() {
        return "404错误,不存在";
    }

    @RequestMapping(produces = requestTypeJson, consumes = requestTypeJson)
    public Map<String, Object> errorJson404() {
        Map<String, Object> map = new HashMap<>();
        map.put("code", 404);
        map.put("msg", "不存在");
        return map;
    }
}

6.5、自定义业务异常类

6.5.1、自定义异常类

自定义异常类需要继承 Exception(异常)类。这里继承 RuntimeException。

package com.example.test3.exception;

public class BusinessException extends RuntimeException {
    // 自定义错误码
    private Integer code;

    // 自定义构造器,必须输入错误码及内容
    public BusinessException(int code, String msg) {
        super(msg);
        this.code = code;
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }
}

6.5.2、自定义全局捕获异常

package com.example.test3.exception;

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;
import java.util.Map;

@ControllerAdvice
public class CustomerBusinessException {
    /**
     * 自定义业务:处理业务异常
     */

    @ResponseBody
    @ExceptionHandler(BusinessException.class)
    public Map<String, Object> businessExceptionHandler(BusinessException e) {
        Map<String, Object> map = new HashMap<>();
        map.put("code", e.getCode());
        map.put("msg", e.getMessage());
        // 此处省略发生异常进行日志记录的代码
        return map;
    }
}

6.5.3、测试自定义异常类

package com.example.test3.controller;

import com.example.test3.exception.BusinessException;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestException {

    @RequestMapping("/BusinessException")
    public String testRestponseStatusExcrptionResolver(@RequestParam("i") Integer i) {
        if (i == 0) {
            throw new BusinessException(600, "自定义业务错误");
        }
        return "Success";
    }
}

7、单元测试

7.1、了解回归测试框架 JUnit4

  • @BeforeClass

在所有测试单元前执行一次,一般用来初始化整体的代码,且该方法时静态的,所以当测试类被加载后就接着运行它,而且在内存中他只会存在一份实例,他比较适合加载配置文件

  • @AfterClass

在所有测试单元后执行一次,一般用来销毁和释放资源,所修饰的方法通常用来对资源管理,如关闭数据库连接(针对所有测试,只执行一次)

  • @Before

会在每个测试方法前执行一次,一般用来初始化方法

  • @After

会在每个测试方法后执行一次,一般用来回滚测试数据

  • @Test

测试用例

  • @Test(timeout=1000)

对测试单元进行限时,这里的“1000”表示若超过 1S 则超时

  • @Test(expected=Exception.class)

指定测试单元期望的得到的异常类。如果执行完成后没有抛出指定的异常,则测试失败

  • @Ignore

忽略的测试方法,如果用于修饰类,则忽略整个类

  • @RunWith

在 JUnit 中有很多 Runner,它们负责调用测试代码。每个Runner 都有特殊功能,应根据需要选择不同的 Runner 来运行测试代码

  • @Transactional

测试的回滚,如自动注入的SQL命令等(如果MySQL引擎不是InnoDB,回滚会失败)。

7.2、了解 assertThat

7.3、Controller 层的单元测试

7.3.1、编写 Controller 方法

@CrossOrigin
@Fields("get请求:获取数据")
@RequestMapping("/getData")
@ResponseBody
public CompareResult generateData() {
    List<excelData> data = new ArrayList<>();
    for (int i = 0; i < 20; i++) {
        data.add(new excelData("AssistantScreen_" + i, "应用中文名称_" + i, true, "责任团队", "1.61.16", "1.53.6"));
    }
    return new CompareResult(0, "ok", data);
}

7.3.2、编写测试

package com.example.test3.controller;

import org.junit.Before;
import org.junit.Test;
import org.junit.jupiter.api.Assertions;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

@SpringBootTest
@RunWith(SpringRunner.class)
public class TestExcelTest {
    // 启用 Web 上下文
    @Autowired
    private WebApplicationContext webApplicationContext;
    private MockMvc mockMvc;

    @Before
    public void setUp() throws Exception {
        // 使用上下文构造 MockMVC
        mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
    }

    @Test
    public void generateData() throws Exception {
        // 定义检测结果
        Integer statusBase = 200;
        String contentBase = "{\"code\":0,\"msg\":\"ok\",\"data\":[{\"...\"}]}";

        // 得到 MVCResult 自定义验证,执行请求
        MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders
                .get("/getData")
                // 请求类型 JSON
//                .contentType(MediaType.APPLICATION_JSON_UTF8)
                // 传入参数 键值对形式
//                .param("", "")
                // 接受类型,主流浏览器不建议带编码
                .accept(MediaType.APPLICATION_JSON_UTF8))
                // 等同于 Assertions.assertEquals(200, status)
                // 判断返回消息 状态码 是否是 200
                .andExpect(MockMvcResultMatchers.status().isOk())
                // 等同于 Assertions.assertEquals("", content)
                // 判断返回消息 内容 是否是 预期
                .andExpect(MockMvcResultMatchers.content().string(contentBase))
                // 打印 消息头
                .andDo(MockMvcResultHandlers.print())
                // 返回 MVCResult
                .andReturn();

        // 得到返回码
        Integer status = mvcResult.getResponse().getStatus();

        // 得到返回结果
        String content = mvcResult.getResponse().getContentAsString();

        // 断言,判断返回代码是否正确
        Assertions.assertEquals(statusBase, status);

        // 断言,判断返回值是否正确
        Assertions.assertEquals(contentBase, content);
    }
}

  • 测试输出打印消息头的结果
MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /getData
       Parameters = {}
          Headers = [Accept:"application/json;charset=UTF-8"]
             Body = <no character encoding set>
    Session Attrs = {}

Handler:
             Type = com.example.test3.controller.TestExcel
           Method = com.example.test3.controller.TestExcel#generateData()

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Vary:"Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers", Content-Type:"application/json;charset=UTF-8"]
     Content type = application/json;charset=UTF-8
             Body = {"code":0,"msg":"ok","data":[{"..."}]}
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

7.4、Service 层的单元测试

7.4.1、创建实体类

package com.example.test3.bean;

import lombok.Data;

import java.io.Serializable;

@Data
public class User implements Serializable {
    private Integer id;
    private String name;
}

7.4.2、创建服务类

package com.example.test3.service;

import com.example.test3.bean.User;
import org.springframework.stereotype.Service;

@Service
public class UserService {
    public User getUser() {
        User user = new User();
        user.setId(1);
        user.setName("王五");
        return user;
    }
}

7.4.3、编写测试

package com.example.test3.service;

import com.example.test3.bean.User;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class UserServiceTest {

    @Autowired
    private UserService userService;

    @Test
    void getUser() {
//        UserService userService = new UserService(); // 采用自动注入方式也可以
        User user = userService.getUser();
        Assertions.assertEquals(1, user.getId());
        Assertions.assertEquals("张三", user.getName());
    }
}

7.5、Repository 层的单元测试

Repository 层主要用于对数据的增、删、改、查操作。它相当于仓库管理员的进出货操作。

package com.example.test3.repository;

import com.example.demo.entity.Card;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
public class CardRepositoryTest {
    @Autowired
    private CardRepository cardRepository;

    @Test
    public void testQuery() {
        // 查询操作
        List<Card> list = cardRepository.findAll();
        for (Card card : list) {
            System.out.println(card);
        }
    }

    @Test
    public void testRollBank() {
        // 查询操作
        Card card = new Card();

        card.setNum(3);
        cardRepository.save(card);
    }
}

7.6、配置类 + 自动注入 + 参数化 单元测试

7.6.1、方法一,直接写入

package com.example.test3.configuration;

import com.example.test3.bean.User;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;


@SpringBootTest
class UserConfigTest {

    @Autowired
    private ApplicationContext applicationContext;


//    @Test
    @ParameterizedTest
    @ValueSource(strings = {"user1", "userBean", "myBean"}) // 复用 2.3 配置类
    void user1(String BeanName) {
        // 实例化User对象,通过上下文获取Bean对象user1
        User user = (User) applicationContext.getBean(BeanName);
        // 在控制台中打印User数据
        System.out.println(user);
    }
}

7.6.2、方法二,自动搜索

7.6.2.1、规范 @Bean 命名,用于自动匹配(2.3直接修改)

package com.example.test3.configuration;

import com.example.test3.bean.User;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class UserConfig {

    @Bean("TestBean_user_01")
    public User user1(){
        User user = new User();
        user.setId(1);
        user.setName("张三");
        return user;
    }

    @Bean("TestBean_user_02")
    public User user2(){
        User user = new User();
        user.setId(2);
        user.setName("赵四");
        return user;
    }

    @Bean({"TestBean_user_03","testBean","myBean"})
    public User user3(){
        User user = new User();
        user.setId(3);
        user.setName("王五");
        return user;
    }
}

7.6.2.2、创建 @Bean 搜索方法

package com.example.test3.JUnit5;

import org.springframework.context.ApplicationContext;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.stream.Stream;


public class ParameterizedMethod {

    protected static Stream<String> getTestBeanName(ApplicationContext applicationContext, String TestBeanName) {
        ArrayList<String> bean_list = new ArrayList<>();
        String[] list_bean = applicationContext.getBeanDefinitionNames();
        for (String beanName : list_bean) {
            if (beanName.startsWith("TestBean_")) {
                if (beanName.contains("_" + TestBeanName + "_")) {
                    bean_list.add(beanName);
                }
            }
        }
        System.out.println(Arrays.toString(bean_list.toArray(new String[0])));
        return Stream.of(bean_list.toArray(new String[0]));
    }
}

7.6.2.3、编写测试

package com.example.test3.configuration;

import com.example.test3.JUnit5.ParameterizedMethod;
import com.example.test3.bean.User;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;

import java.util.stream.Stream;

@TestInstance(TestInstance.Lifecycle.PER_CLASS) // 测试实例生命周期,每个测试类将创建一个新的测试实例,@MethodSource 必要条件
@SpringBootTest
class UserConfigTest extends ParameterizedMethod {

    @Autowired
    public ApplicationContext applicationContext;

    public final String TestBeanName = "user";

    // 自动搜索 前缀是 TestBean_ + TestBeanName 的 @Bean 名称。
    Stream<String> stringProvider() {
        return ParameterizedMethod.getTestBeanName(applicationContext, TestBeanName);
    }

    @ParameterizedTest
    @MethodSource("stringProvider") // 也可以引用其他文件的方法,但是不能带参数
    void user2(String BeanName) {
        // 实例化User对象,通过上下文获取Bean对象user1
        User user = (User) applicationContext.getBean(BeanName);
        // 在控制台中打印User数据
        System.out.println(user);
    }
    
}
上一篇:Solon 开发,四、Bean 扫描的三种方式


下一篇:springboot快速入门-9.项目启动后读取数据库并放到Redis