Spring全家桶——SpringBoot之AOP详解

面向方面编程(AOP)通过提供另一种思考程序结构的方式来补充面向对象编程(OOP)。

OOP中模块化的关键单元是类,而在AOP中,模块化单元是方面。

准备工作

首先,使用AOP要在build.gradle中加入依赖

//引入AOP依赖
compile "org.springframework.boot:spring-boot-starter-aop:${springBootVersion}"

然后在application.yml中加入

spring:
aop:
proxy-target-class: true

1.@Pointcut 切入点

定义一个切点

例如我们要在一个方法加上切入点,根据方法的返回的对象,方法名,修饰词来写成一个表达式或者是具体的名字

我们现在来定义一个切点

package com.example.aop;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component; /**
* 类定义为切面类
*/
@Aspect
@Component
public class AopTestController {
private static final Logger logger = LoggerFactory.getLogger(AopTestController.class);
/**
* 定义一个切点
*/
@Pointcut(value = "execution(public String test (..))")
public void cutOffPoint() {
}
}

这里的切点定义的方法是

    @GetMapping("hello")
public String test(){
logger.info("欢迎关注Java知音");
return "i love java";
}

如果你想写个切入点在所有返回对象为Area的方法,如下

@Pointcut("execution(public com.example.entity.Area (..))")

等很多写法,也可以直接作用在某些包下

注意:private修饰的无法拦截

2.@Before前置通知

在切入点开始处切入内容

在之前的AopTestController类中加入对test方法的前置通知

    @Before("cutOffPoint()")
public void beforeTest(){
logger.info("我在test方法之前执行");
}

这里@Before里的值就是切入点所注解的方法名

在方法左侧出现的图标跟过去以后就是所要通知的方法

这里就是配置正确了,我们来浏览器调用一下方法

联想一下,这样的效果可以用在哪里,想像如果要扩展一些代码,在不需要动源代码的基础之上就可以进行拓展,美滋滋

3.@After 后置通知

和前置通知相反,在切入点之后执行

    @After("cutOffPoint()")
public void doAfter(){
logger.info("我是在test之后执行的");
}

控制台执行结果

这里定义一个通知需要重启启动类,而修改通知方法的内容是可以热部署的

4.@Around环绕通知

和前两个写法不同,实现的效果包含了前置和后置通知

当使用环绕通知时,proceed方法必须调用,否则拦截到的方法就不会再执行了

环绕通知=前置+目标方法执行+后置通知,proceed方法就是用于启动目标方法执行的

    ThreadLocal<Long> startTime = new ThreadLocal<>();
@Around("cutOffPoint()")
public Object doAround(ProceedingJoinPoint pjp){
startTime.set(System.currentTimeMillis());
logger.info("我是环绕通知执行");
Object obj;
try{
obj = pjp.proceed();
logger.info("执行返回值 : " + obj);
logger.info(pjp.getSignature().getName()+"方法执行耗时: " + (System.currentTimeMillis() - startTime.get()));
} catch (Throwable throwable) {
obj=throwable.toString();
}
return obj;
}

执行结果:

1.环绕通知可以项目做全局异常处理

2.日志记录

3.用来做数据全局缓存

4.全局的事物处理 等

5.@AfterReturning

切入点返回结果之后执行,也就是都前置后置环绕都执行完了,这个就执行了

    /**
* 执行完请求可以做的
* @param result
* @throws Throwable
*/
@AfterReturning(returning = "result", pointcut = "cutOffPoint()")
public void doAfterReturning(Object result) throws Throwable {
logger.info("大家好,我是@AfterReturning,他们都秀完了,该我上场了");
}

执行结果

应用场景可以用来在订单支付完成之后就行二次的结果验证,重要参数的二次校验,防止在方法执行中的时候参数被修改等等

6.@AfterThrowing

这个是在切入执行报错的时候执行的

    // 声明错误e时指定的抛错类型法必会抛出指定类型的异常
// 此处将e的类型声明为Throwable,对抛出的异常不加限制
@AfterThrowing(throwing = "e",pointcut = "cutOffPoint()")
public void doAfterReturning(Throwable e) {
logger.info("大家好,我是@AfterThrowing,他们犯的错误,我来背锅");
logger.info("错误信息"+e.getMessage());
}

在其他切入内容中随意整个错误出来,制造一个环境

下面是@AfterThrowing的执行结果

7.AOP用在全局异常处理

定义切入点拦截ResultBean或者PageResultBean

    @Pointcut(value = "execution(public com.example.beans.PageResultBean *(..)))")
public void handlerPageResultBeanMethod() {
} @Pointcut(value = "execution(public com.example.beans.ResultBean *(..)))")
public void handlerResultBeanMethod() {
}

下面是AopController.java

package com.example.aop;

import com.example.beans.PageResultBean;
import com.example.beans.ResultBean;
import com.example.entity.UnloginException;
import com.example.exception.CheckException;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component; /**
* 使用@Aspect注解将此类定义为切面类
* 根据晓风轻著的ControllerAOP所修改
* 晓风轻大佬(很大的佬哥了):https://xwjie.github.io/
*/
@Aspect
@Component
public class AopController { private static final Logger logger = LoggerFactory.getLogger(AopController.class); ThreadLocal<ResultBean> resultBeanThreadLocal = new ThreadLocal<>();
ThreadLocal<PageResultBean<?>> pageResultBeanThreadLocal = new ThreadLocal<>();
ThreadLocal<Long> start = new ThreadLocal<>(); /**
* 定义一个切点
*/
@Pointcut(value = "execution(public com.example.beans.PageResultBean *(..)))")
public void handlerPageResultBeanMethod() {
} @Pointcut(value = "execution(public com.example.beans.ResultBean *(..)))")
public void handlerResultBeanMethod() {
} @Around("handlerPageResultBeanMethod()")
public Object handlerPageResultBeanMethod(ProceedingJoinPoint pjp) {
start.set(System.currentTimeMillis());
try {
pageResultBeanThreadLocal.set((PageResultBean<?>)pjp.proceed());
logger.info(pjp.getSignature() + " 方法执行耗时:" + (System.currentTimeMillis() - start.get()));
} catch (Throwable e) {
ResultBean<?> resultBean = handlerException(pjp , e);
pageResultBeanThreadLocal.set(new PageResultBean<>().setMsg(resultBean.getMsg()).setCode(resultBean.getCode()));
}
return pageResultBeanThreadLocal.get();
} @Around("handlerResultBeanMethod()")
public Object handlerResultBeanMethod(ProceedingJoinPoint pjp) {
start.set(System.currentTimeMillis());
try {
resultBeanThreadLocal.set((ResultBean<?>)pjp.proceed());
logger.info(pjp.getSignature() + " 方法执行耗时:" + (System.currentTimeMillis() - start.get()));
} catch (Throwable e) {
resultBeanThreadLocal.set(handlerException(pjp , e));
}
return resultBeanThreadLocal.get();
}
/**
* 封装异常信息,注意区分已知异常(自己抛出的)和未知异常
*/
private ResultBean<?> handlerException(ProceedingJoinPoint pjp, Throwable e) { ResultBean<?> result = new PageResultBean();
logger.error(pjp.getSignature() + " error ", e); // 已知异常
if (e instanceof CheckException) {
result.setMsg(e.getLocalizedMessage());
result.setCode(ResultBean.FAIL);
} else if (e instanceof UnloginException) {
result.setMsg("Unlogin");
result.setCode(ResultBean.NO_LOGIN);
} else {
result.setMsg(e.toString());
result.setCode(ResultBean.FAIL);
}
return result;
}
}

用上面的环绕通知可以对所有返回ResultBean或者PageResultBean的方法进行切入,这样子就不用在业务层去捕捉错误了,只需要去打印自己的info日志

看下面一段代码

    @Transactional
@Override
public int insertSelective(Area record) {
record.setAddress("test");
record.setPostalcode(88888);
record.setType(3);
int i=0;
try {
i = areaMapper.insertSelective(record);
}catch (Exception e){
logger.error("AreaServiceImpl insertSelective error:"+e.getMessage());
}
return i;
}

假如上面的插入操作失败出错了?

你认为会回滚吗?

答案是:不会。

为什么?

因为你把错误捕捉了,事物没检测到异常就不会回滚。

那么怎么才能回滚呢?

在catch里加throw new RuntimeException().

可是那么多业务方法每个设计修改的操作都加,代码繁琐,怎么进行处理呢?

在这里用到上面的AOP切入处理,错误不用管,直接抛,抛到控制层进行处理,这样的话,接口调用的时候,出错了,接口不会什么都不返回,而是会返回给你错误代码,以及错误信息,便于开发人员查错

8.以上用的是log4j2的日志处理

先移除springboot自带的log日志处理

在build.gradle中增加

configurations {
providedRuntime
// 去除SpringBoot自带的日志
all*.exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'
}
ext {
springBootVersion = '2.0.1.RELEASE'
}
dependencies {
compile "org.springframework.boot:spring-boot-starter-log4j2:${springBootVersion}"
}

然后在application.yml中增加

#显示mysql执行日志
logging:
level:
com:
example:
dao: debug
config: classpath:log4j2-spring.xml

log4j2-spring.xml

<?xml version="1.0" encoding="UTF-8"?>
<!--日志级别以及优先级排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL -->
<!--Configuration后面的status,这个用于设置log4j2自身内部的信息输出,可以不设置,当设置成trace时,你会看到log4j2内部各种详细输出-->
<!--monitorInterval:Log4j能够自动检测修改配置 文件和重新配置本身,设置间隔秒数-->
<configuration status="INFO" monitorInterval="30">
<!--先定义所有的appender-->
<appenders>
<!--这个输出控制台的配置-->
<console name="Console" target="SYSTEM_OUT">
<!--输出日志的格式-->
<PatternLayout pattern="%highlight{[ %p ] [%-d{yyyy-MM-dd HH:mm:ss}] [ LOGID:%X{logid} ] [%l] %m%n}"/>
</console> <!--文件会打印出所有信息,这个log每次运行程序会自动清空,由append属性决定,这个也挺有用的,适合临时测试用-->
<File name="Test" fileName="logs/test.log" append="false">
<PatternLayout pattern="%highlight{[ %p ] %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] [%l] %m%n}"/>
</File> <RollingFile name="RollingFileInfo" fileName="logs/log.log" filePattern="logs/info.log.%d{yyyy-MM-dd}">
<!-- 只接受level=INFO以上的日志 -->
<ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>
<PatternLayout pattern="%highlight{[ %p ] [%-d{yyyy-MM-dd HH:mm:ss}] [ LOGID:%X{logid} ] [%l] %m%n}"/>
<Policies>
<TimeBasedTriggeringPolicy modulate="true" interval="1"/>
<SizeBasedTriggeringPolicy/>
</Policies>
</RollingFile> <RollingFile name="RollingFileError" fileName="logs/error.log" filePattern="logs/error.log.%d{yyyy-MM-dd}">
<!-- 只接受level=WARN以上的日志 -->
<Filters>
<ThresholdFilter level="warn" onMatch="ACCEPT" onMismatch="DENY" />
</Filters>
<PatternLayout pattern="%highlight{[ %p ] %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] [%l] %m%n}"/>
<Policies>
<TimeBasedTriggeringPolicy modulate="true" interval="1"/>
<SizeBasedTriggeringPolicy/>
</Policies>
</RollingFile> </appenders> <!--然后定义logger,只有定义了logger并引入的appender,appender才会生效-->
<loggers>
<!--过滤掉spring和mybatis的一些无用的DEBUG信息-->
<logger name="org.springframework" level="INFO"></logger>
<logger name="org.mybatis" level="INFO"></logger>
<root level="all">
<appender-ref ref="Console"/>
<appender-ref ref="Test"/>
<appender-ref ref="RollingFileInfo"/>
<appender-ref ref="RollingFileError"/>
</root>
</loggers>
</configuration>

之后在你要打印日志的类中增加

private static final Logger logger = LoggerFactory.getLogger(你的类名.class);

    public static void main(String[] args) {
logger.error("error级别日志");
logger.warn("warning级别日志");
logger.info("info级别日志");
}

有了日志后就很方便了,在你的方法接收对象时打印下,然后执行了逻辑之后打印下,

出错之后很明确了,就会很少去Debug的,养成多打日志的好习惯,多打印一点info级别的日志,用来在开发环境使用,在上线的时候把打印的最低级别设置为warning,这样你的info级别日志也不会影响到项目的重要Bug的打印

写这个博客的时候我也在同时跑着这个项目,有时候会出现一些错误,例如jar包版本,业务层引用无效,AOP设置不生效等等,也同时在排查解决,如果你遇到了同样的错误,可以去我的GitHub联系我,如小弟有时间或许也能帮到你,谢谢

Github地址:https://github.com/cuifuan

Spring全家桶——SpringBoot之AOP详解的更多相关文章

  1. Spring全家桶系列–SpringBoot之AOP详解

    //本文作者:cuifuan //本文将收录到菜单栏:<Spring全家桶>专栏中 面向方面编程(AOP)通过提供另一种思考程序结构的方式来补充面向对象编程(OOP). OOP中模块化的关 ...

  2. Spring全家桶——SpringBoot渐入佳境

    Spring全家桶系列--SpringBoot渐入佳境 萌新:小哥,我在实体类写了那么多get/set方法,看着很迷茫 小哥:那不是可以自动生成吗? 萌新:虽然可以自动生成,但是如果我要修改某个变量的 ...

  3. Spring全家桶–SpringBoot Rest API

    Spring Boot通过提供开箱即用的默认依赖或者转换来补充Spring REST支持.在Spring Boot中编写RESTful服务与SpringMVC没有什么不同.总而言之,基于Spring ...

  4. 拥有 GitHub 开源项目的小伙伴,免费申请 JetBrains 全家桶的全流程详解

    工欲善其事,必先利其器.如果您想要学习 Java.PHP.Ruby.Python.JavaScript.Objective-C..NET 中的任何一种开发技术,国际知名且屡获殊荣的 JetBrains ...

  5. Spring学习(八)AOP详解

    文章更新时间:2020/04/06 一.一个例子 在上面的例子中,包租婆的核心业务就是签合同,收房租,那么这就够了,灰色框起来的部分都是重复且边缘的事,交给中介商就好了,这就是 AOP 的一个思想:让 ...

  6. Spring全家桶系列–[SpringBoot入门到跑路]

    //本文作者:cuifuan Spring全家桶————[SpringBoot入门到跑路] 对于之前的Spring框架的使用,各种配置文件XML.properties一旦出错之后错误难寻,这也是为什么 ...

  7. 10分钟详解Spring全家桶7大知识点

    Spring框架自2002年诞生以来一直备受开发者青睐,它包括SpringMVC.SpringBoot.Spring Cloud.Spring Cloud Dataflow等解决方案.有人亲切的称之为 ...

  8. Spring全家桶一一SpringBoot与Mybatis

    Spring全家桶系列一一SpringBoot与Mybatis结合 本文授权"Java知音"独家发布. Mybatis 是一个持久层ORM框架,负责Java与数据库数据交互,也可以 ...

  9. Spring全家桶系列–SpringBoot渐入佳境

    //本文作者:cuifuan //本文将收录到菜单栏:<Spring全家桶>专栏中 首发地址:https://www.javazhiyin.com/20913.html 萌新:小哥,我在实 ...

随机推荐

  1. 【JAVA基础】07 面向对象2

    1. 代码块的概述和分类 面试的时候会问,开发不用或者很少用 代码块概述 在Java中,使用 {} 括起来的代码被称为代码块. 代码块分类 根据其位置和声明的不同,可以分为局部代码块,构造代码块,静态 ...

  2. 用VC实现洪水攻击程序

    本文为原创,如需转载,请注明作者和出处,谢谢! 一.             什么是洪水攻击 洪水之猛.势不可挡.如果将洪水比作对计算机的攻击,那大家可以想象得出,攻击是多的猛烈. 在安全领域所指的洪 ...

  3. P3842 [TJOI2007]线段

    最近多刷些dp,觉得这个算不上蓝题   在一个\(n\times n\)的平面上,在每一行中有一条线段,第\(i\)行的线段的左端点是\((i, L_i)\),右端点是\((i, R_i)\),其中\ ...

  4. JDK 配置环境变量

    1.配置环境变量 右击 我的电脑 --> 属性 --> 高级系统设置 --> 高级 --> 环境变量 在系统变量里新建 JAVA_HOME 变量,变量值如下 D:\work_s ...

  5. vue 比 js的强大

    //js写留言板 <!DOCTYPE html><html lang="en"><head> <meta charset="UT ...

  6. CTF-Reverse-[GXYCTF2019]luck_guy

    CTF-Reverse-[GXYCTF2019]luck_guy 博客说明 文章所涉及的资料来自互联网整理和个人总结,意在于个人学习和经验汇总,如有什么地方侵权,请联系本人删除,谢谢!本文仅用于学习与 ...

  7. 第 4 篇:用类视图实现首页 API

    作者:HelloGitHub-追梦人物 文中所涉及的示例代码,已同步更新到 HelloGitHub-Team 仓库 django-rest-framework 类视图拓展自 django 的类视图,只 ...

  8. 记一次sqoop安装后测试的问题

    运行命令: sqoop import --connect "jdbc:mysql://x.x.x.x:3306/intelligent_qa_bms?useUnicode=true& ...

  9. 使用RSS订阅喜欢的微博博主

    获取uid方法:进入博主微博主页,使用控制台执行 /uid=(\d+)/. exec(document.querySelector('.opt_box .btn_bed').getAttribute( ...

  10. 【FPGA篇章六】FPGA编译向导:详解编译预处理功能

    欢迎大家关注我的微信公众账号,支持程序媛写出更多优秀的文章 Verilog HDL语言和C语言一样也提供了编译预处理功能. Verilog HDL允许在程序中使用特殊的编译预处理语句. 在编译时,通常 ...