1、什么是AOP
1.1、对AOP的初印象
首先先给出一段比较专业的术语(来自百度):
在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方
式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个
热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑
的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高
了开发的效率。
然后我们举一个比较容易理解的例子(来自:Spring 之 AOP):
要理解切面编程,就需要先理解什么是切面。用刀把一个西瓜分成两瓣,切开的切口就是切面;炒菜,锅与炉子共同来完成炒菜,锅与炉子就是切面。web层级设计中,web层->网关层->服务层->数据层,每一层之间也是一个切面。编程中,对象与对象之间,方法与方法之间,模块与模块之间都是一个个切面。
我们一般做活动的时候,一般对每一个接口都会做活动的有效性校验(是否开始、是否结束等等)、以及这个接口是不是需要用户登录。按照正常的逻辑,我们可以这么做。
这有个问题就是,有多少接口,就要多少次代码copy。对于一个“懒人”,这是不可容忍的。好,提出一个公共方法,每个接口都来调用这个接口。这里有点切面的味道了。
同样有个问题,我虽然不用每次都copy代码了,但是,每个接口总得要调用这个方法吧。于是就有了切面的概念,我将方法注入到接口调用的某个地方(切点)。
这样接口只需要关心具体的业务,而不需要关注其他非该接口关注的逻辑或处理。
红框处,就是面向切面编程。
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
里面定义的程序点具体要做的操作
,它通过before
、after
和around
来区别是在每个 joint point 之前、之后还是代替执行的代码。 -
Target(目标对象):织入
Advice
的目标对象
。 -
Weaving(织入):将
Aspect
和其他对象连接起来
, 并创建Adviced object
的过程
1.2.2、然后举一个容易理解的例子:
看完了上面的理论部分知识, 我相信还是会有不少朋友感觉到 AOP 的概念还是很模糊, 对 AOP 中的各种概念理解的还不是很透彻. 其实这很正常, 因为 AOP 中的概念实在是太多了, 我当时也是花了老大劲才梳理清楚的.
- 下面我以一个简单的例子来比喻一下
AOP
中Aspect
,Joint point
,Pointcut
与Advice
之间的关系.
让我们来假设一下, 从前有一个叫爪哇的小县城, 在一个月黑风高的晚上, 这个县城中发生了命案. 作案的凶手十分狡猾, 现场没有留下什么有价值的线索. 不过万幸的是, 刚从隔壁回来的老王恰好在这时候无意中发现了凶手行凶的过程, 但是由于天色已晚, 加上凶手蒙着面, 老王并没有看清凶手的面目, 只知道凶手是个男性, 身高约七尺五寸. 爪哇县的县令根据老王的描述, 对守门的士兵下命令说: 凡是发现有身高七尺五寸的男性, 都要抓过来审问. 士兵当然不敢违背县令的命令, 只好把进出城的所有符合条件的人都抓了起来.
来让我们看一下上面的一个小故事和 AOP 到底有什么对应关系.
首先我们知道, 在 Spring AOP
中 Joint 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 上的。同理, 对比到我们的例子中, 抓过来审问 这个动作就是对作用于那些满足 男性, 身高约七尺五寸 的爪哇的小县城里的百姓。 -
Aspect :
Aspect
是point cut
与Advice
的组合, 因此在这里我们就可以类比: “根据老王的线索, 凡是发现有身高七尺五寸的男性, 都要抓过来审问” 这一整个动作可以被认为是一个 Aspect。
最后是一个描述这些概念之间关系的图:
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 的步骤
- 新建类,实现 Filter 抽象类。
- 重写 init、doFilter、destroy方法。
- 在 Spring Boot 入口中添加注解 @ServletComponentScan,以注册Filter。
在重写3个方法后,还可以进一步修改 request 参数使用的封装方式,如:
- 编写 ParameterRequestWrqpper 类继承 HTTPServletRequestWrapper类。
- 编写 ParameterRequestWrqpper 类的构造器。
- 在构造器中覆盖父类构造器,并将 request.getParameterMap 加入子类的成员变量。
- 编写 addParam 方法。
- 修改参数并调用 ParameterRequestWrapper 实例,并保存 param。
- 调用 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种类型:
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、单元测试
- JUnit5基本介绍
- junit5 入门系列教程-01-junit5 简单入门例子
- junit5 入门系列教程-02-junit5 注解详解
- junit5 入门系列教程-03-junit5 测试类和方法 {@Test、@RepeatedTest、@ParameterizedTest、@TestFactory或@TestTemplate}
- junit5 入门系列教程-04-junit5 展现名称(@DisplayName)
- junit5 入门系列教程-05-junit5 断言(assert)
- junit5 入门系列教程-06-junit5 假设(Assumptions)
- junit5 入门系列教程-07-junit5 禁用(@Disabled)
- junit5 入门系列教程-08-junit5 条件执行(@EnabledXXX, @DisabledXXX)
- junit5 入门系列教程-09-junit5 标签和过滤(@Tag)
- junit5 入门系列教程-10-junit5 测试实例生命周期(@TestInstance)
- junit5 入门系列教程-11-junit5 内嵌测试(@Nested)
- junit5 入门系列教程-12-junit5 依赖注入构造器、方法
- junit5 入门系列教程-13-junit5 测试接口及默认方法
- junit5 入门系列教程-14-junit5 重复测试(@RepeatedTest)
- junit5 入门系列教程-15-junit5 参数化测试(@ParameterizedTest)
- junit5 入门系列教程-16-junit5 测试模板(@TestTemplate)
- junit5 入门系列教程-17-junit5 动态测试(DynamicTest)
- junit5 入门系列教程-18-junit5 拓展模块-总览(Extend Model)
- junit5 入门系列教程-19-junit5 拓展实体-注册(Register Extension)
- junit5 入门系列教程-20-junit5 拓展实体-条件测试
- junit5 入门系列教程-21-junit5 拓展实体-测试实例后处理(Instance Post-processing)
- junit5 入门系列教程-22-junit5 拓展实体-参数化测试解决方案(Parameter Resolution)
- junit5 入门系列教程-23-junit5 拓展实体-异常处理器(Exception Handle)
- junit5 入门系列教程-24-junit5 拓展实体-测试上下文(Test Context)
- junit5 入门系列教程-25-junit5 拓展实体-存储状态(Keeping State in Extensions)
- junit5 入门系列教程-26-junit5 拓展实体-工具类
- junit5 入门系列教程-27-junit5 拓展实体-用户代码和扩展的相对执行顺序
- junit5 入门系列教程-28-junit5 拓展实体-测试生命周期回调
- junit5 入门系列教程-29-junit5 拓展实体-Junit 平台启动器API(JUnit Platform Launcher API)
- junit5 入门系列教程-30-junit5 实战例子 junit performance
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);
}
}