Spring—AOP(面向切面编程)
一、AOP是什么?
AOP(Aspect Oriented Programming)面向切面编程,是OOP的一种重要补充,也是Spring的另一个核心。
OOP是基于封装、继承、多态的编程思想,关注类之间纵向关系;AOP关注横向关系,能够为多个相互没有关系,又都需要某些共同功能的类,提供一些通用服务(如:日志、权限、缓存、事务等)。
二、AOP有什么用?
代码解耦,可以把与类的核心业务无关,又都需要的功能封装起来,让类只关注自己的核心业务,分离了系统的核心业务和非核心业务。
三、AOP的术语
术语 | 描述 |
---|---|
Aspect | 切面:对哪些方法进行拦截,拦截后怎么处理,这些关注点称之为切面。 |
Joinpoint | 连接点:被拦截到的点,因为Spring只支持方法类型的连接点,所以在Spring中连接点指的就是被拦截到的方法,实际上连接点还可以是字段或者构造器。 |
Pointcut | 切入点:对连接点进行拦截的定义。 |
Advice | 通知:所谓通知指的就是拦截到连接点后要执行的代码,通知分为前置、后置、异常、最终、环绕通知五类。 |
Target | 目标对象:代理的目标对象,将切面应用到目标对象并导致代理对象创建的过程。 |
Introduction/Weave | 引入/织入:在不修改代码的前提下,引入可以在运行期间为类动态地添加一些方法或字段。 |
通知Advice的类型
四、AOP配置版案例
- 引入依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.5</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.5</version>
</dependency>
- 编写通知类
import org.aspectj.lang.ProceedingJoinPoint;
public class Main {
public void start(){
System.out.println("欢迎光临!");
}
public void shopping(ProceedingJoinPoint joinPoint) throws Throwable{
System.out.println("导购员:请问需要帮忙吗?");
joinPoint.proceed();
System.out.println("好的,您自己随便看");
}
public void checkout(){
System.out.println("这是您的账单");
}
public void end(){
System.out.println("慢走不送,期待您的下次光临!");
}
}
- 编写普通类
public class GoodsService {
public void shopping() {
System.out.println("顾客在购物");
}
}
- 编写配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="goodsService" class="com.hhx.springaop01.service.GoodsService"></bean>
<!-- 配置包的扫描-->
<context:component-scan base-package="com.hhx.springaop01"></context:component-scan>
<!-- 配置通知类-->
<bean id="main" class="com.hhx.springaop01.utils.Main"></bean>
<aop:config>
<!-- 配置切入点-->
<aop:pointcut id="shopping" expression="execution(* com.hhx.springaop01.service.*Service.*(..))"/>
<!-- 配置切面 ref是通知类的bean-->
<aop:aspect id="aspect" ref="main">
<aop:before method="start" pointcut-ref="shopping"></aop:before>
<aop:around method="shopping" pointcut-ref="shopping"></aop:around>
<aop:after-returning method="checkout" pointcut-ref="shopping"></aop:after-returning>
<aop:after method="end" pointcut-ref="shopping"></aop:after>
</aop:aspect>
</aop:config>
</beans>
- 编写测试类
public class TestAOP01 {
public static void main(String[] args) {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring-aop.xml");
GoodsService goodsService = context.getBean(GoodsService.class);
goodsService.shopping();
}
}
- 测试结果
注:aop:pointcut的expression属性:通过表达式控制切面应用的范围
语法:execution(访问修饰符 返回值 包名.类名.方法名(参数类型,参数类型…))
通配符:① *代表任意长度的字符 ② … 代替子包或任意参数
五、AOP注解版案例
注解 | 描述 |
---|---|
@Aspect | 切面,配置到切面类上 |
@PointCut(“表达式”) | 配置切入点,加在方法上 |
@Before | 配置前置通知方法 |
@After | 配置后置通知方法 |
@Around | 配置环绕通知方法 |
@AfterReturning | 配置后置返回值通知方法 |
@AfterThrowing | 配置后置抛出异常通知方法 |
- 注入依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>
- 创建配置类
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
/**
* 配置类
*/
@EnableAspectJAutoProxy
@ComponentScan(basePackages = "com.hhx.springaop02")
@Configuration
public class AOPConfig {
}
- 配置切面
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
/**
* 商店切面
*/
@Component
@Aspect
public class Main {
@Pointcut("execution(* com.hhx.springaop02.*.*(..))")
public void pointCut(){
}
@Before("pointCut()")
public void start(){
System.out.println("欢迎光临!");
}
@Around("pointCut()")
public void shopping(ProceedingJoinPoint joinPoint) throws Throwable{
System.out.println("导购员:请问需要帮忙吗?");
joinPoint.proceed();
System.out.println("好的,您自己随便看");
}
@AfterReturning("pointCut()")
public void checkout(){
System.out.println("这是您的账单");
}
@After("pointCut()")
public void end(){
System.out.println("慢走不送,期待您的下次光临!");
}
}
- 普通类
import org.springframework.stereotype.Component;
@Component
public class GoodsService {
public void shopping() {
System.out.println("顾客在购物");
}
}
- 测试
public class TestAOP02 {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AOPConfig.class);
GoodsService goodsService = context.getBean(GoodsService.class);
goodsService.shopping();
}
}
- 测试结果
从以上案例中注意到:
配置版通知方法执行顺序为:before-around-afterreturning-after
注解版通知方法执行顺序为:around-before-afterreturning-after-around
六、Log4J日志追踪案例
- 注入依赖
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
- 创建log4j.properties
# ROOTER
log4j.rootLogger=DEBUG,CONSOLE,FILE
# CONSOLE
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern=%-d{yyyy-MM-dd HH\:mm\:ss} %-5p %-20c %x %m%n
# FILE
log4j.appender.FILE=org.apache.log4j.RollingFileAppender
log4j.appender.FILE.File=logs.log
log4j.appender.FILE.MaxBackupIndex=20
log4j.appender.FILE.MaxFileSize=10MB
log4j.appender.FILE.layout=org.apache.log4j.PatternLayout
log4j.appender.FILE.layout.ConversionPattern=%-d{yyyy-MM-dd HH\:mm\:ss} %-5p %-20c %x %m%n
# ERROR
log4j.appender.ERR = org.apache.log4j.DailyRollingFileAppender
log4j.appender.ERR.File =error.log
log4j.appender.ERR.Append = true
log4j.appender.ERR.Threshold = ERROR
log4j.appender.file.DatePattern='.'yyyy-MM-dd'.log'
log4j.appender.ERR.layout = org.apache.log4j.PatternLayout
log4j.appender.ERR.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n
##########################################################################################################
#Appender
#日志输出器,配置日志的输出级别、输出位置等,包括以下几类:
#ConsoleAppender: 日志输出到控制台;
#FileAppender: 输出到文件;
#RollingFileAppender: 输出到文件,文件达到一定阈值时,自动备份日志文件;
#DailyRollingFileAppender:可定期备份日志文件,默认一天一个文件,也可设置为每分钟一个、每小时一个;
#WriterAppender: 可自定义日志输出位置。
#
#日志级别
#一般日志级别包括:ALL,DEBUG, INFO, WARN, ERROR,FATAL,OFF
#Log4J推荐使用:DEBUG, INFO, WARN, ERROR
#
#
#
#Layout:日志输出格式,Log4j提供的layout有以下几种:
#org.apache.log4j.HTMLLayout(以HTML表格形式布局),
#org.apache.log4j.PatternLayout(可以灵活地指定布局模式),
#org.apache.log4j.SimpleLayout(包含日志信息的级别和信息字符串),
#org.apache.log4j.TTCCLayout(包含日志产生的时间、线程、类别等等信息)
#
#输出格式
#Log4J最常用的日志输出格式为:org.apache.log4j.PatternLayOut,其主要参数为:
#%n - 换行
#%m - 日志内容
#%p - 日志级别(FATAL, ERROR,WARN, INFO,DEBUG or custom)
#%r - 程序启动到现在的毫秒数
#%t - 当前线程名
#%d - 日期和时间, 一般使用格式 %d{yyyy-MM-dd HH:mm:ss, SSS}
#%l - 输出日志事件的发生位置, 同 %F%L%C%M
#%F - java 源文件名
#%L - java 源码行数
#%C - java 类名,%C{1} 输出最后一个元素
#%M - java 方法名
#
#Logger log = Logger.getLogger(当前类.class);
#if(log.isDebugEnabled()){
#log.debug("...");
#}
#if(log.isInfoEnabled()){
#log.info("...");
#}
- 编写日志切面
import org.apache.log4j.Logger;
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.springframework.stereotype.Component;
/**
* Log4j日志输出切面
*/
@Aspect
@Component
public class Log4jAspect {
//创建日志对象
private Logger logger = Logger.getLogger(Log4jAspect.class);
//给所有的类的所有方法加日志跟踪
@Pointcut("execution(* com.hhx.springaop03.*.*(..))")
public void logPointCut(){
}
//配置环绕通知
@Around("logPointCut()")
public Object aroundLog(ProceedingJoinPoint joinPoint) throws Throwable{
//记录方法执行时间
long start = System.currentTimeMillis();
//打印方法名称
if (logger.isDebugEnabled()){
logger.debug("当前执行方法:"+joinPoint.getSignature().getName());
}
//打印参数
Object[] args = joinPoint.getArgs();
for (Object arg:args) {
if (logger.isDebugEnabled()){
logger.debug("参数:"+arg);
}
}
logger.debug("开始执行方法……");
//打印返回值
Object result = joinPoint.proceed();
logger.debug("结束执行方法……");
if(logger.isDebugEnabled()){
logger.debug("方法返回值:"+result);
}
//打印执行时间
long end = System.currentTimeMillis();
if (logger.isDebugEnabled()){
logger.debug("方法执行时间:"+(end-start));
}
return result;
}
}
- 测试
public class TestLog {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AOPConfig.class);
//调用普通类方法
GoodsService goodsService = context.getBean(GoodsService.class);
goodsService.shopping();
}
}
- 测试结果
控制台输出:
日志文档(自动生成):