学习目标
-
能够说出动态代理的两种方式
-
能够理解AOP的相关概念
-
能够说出AOP相关术语的含义
-
能够编写spring AOP中不同通知类型代码
-
能够理解spring AOP中的注解
spring的AOP概述【了解】
AOP介绍
AOP(Aspect Oriented Programming),即面向切面编程。**通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件系统开发中的一个热点,也是spring框架的一个重点。**利用AOP可以实现业务逻辑各个部分的隔离,从而使得业务逻辑各个部分的耦合性降低,提高程序的可重用性,同时提高开发效率。
简单理解:aop是面向切面编程,是使用动态代理技术,实现在不修改java源代码的情况下,运行时实现方法功能的增强。
AOP的作用及优势
作用
使用动态代理技术,在程序运行期间,不修改java源代码对已有方法功能进行增强。
优势
-
减少重复代码,提高开发效率。
-
统一管理统一调用,方便维护。
AOP实现技术
动态代理技术。
AOP常用术语【掌握】
-
Joinpoint(连接点):在spring中,连接点指的都是方法(指的是那些要被增强功能的候选方法),spring只支持方法类型的连接点。
-
Pointcut(切入点):在运行时已经被spring 的AOP实现了增强的方法。
-
Advice(通知):通知指的是拦截到Joinpoint之后要做的事情。即增强的功能。通知类型:前置通知/后置通知/异常通知/最终通知/环绕通知。
-
Target(目标对象):被代理的对象。比如动态代理案例中的演员。
-
Weaving(织入):织入指的是把增强用于目标对象,创建代理对象的过程。spring采用动态代理织入,AspectJ采用编译期织入和类装载期织入。
-
Proxy(代理):一个类被AOP织入增强后,即产生一个结果代理类。比如动态代理案例中的经纪人。
-
Aspect(切面):切面指的是切入点和通知的结合。
学习spring的AOP需要明确的事情【掌握】
开发阶段(我们做的)
-
根据业务需求,编写核心业务代码
-
把公用代码抽取出来,制作成通知
-
通过配置的方式,建立切入点(业务功能)和通知的关系
运行阶段(spring框架完成)
spring框架监控切入点方法执行。一旦监控到切入点方法被执行,使用动态代理机制,创建目标对象的代理对象,根据通知类型,在代理对象当前执行方法的对应位置,织入通知功能,完成完整的代码逻辑执行。
基于xml的AOP配置【掌握】
创建项目
配置pom.xml,加入依赖包
说明:使用spring的aop,需要加入IOC基础框架包和spring的AOP支持包。
IOC:
AOP:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cn.itheima</groupId>
<artifactId>spring-day03-03aop-xml</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.30</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.1</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>${spring.version}</version>
</dependency>
<!--aopalliance包-->
<dependency>
<groupId>aopalliance</groupId>
<artifactId>aopalliance</artifactId>
<version>${aopalliance.version}</version>
</dependency>
</dependencies>
</project>
编写业务层对象
这一步骤,对应开发阶段的第一件事情:根据业务需求,编写业务代码。
- 编写客户service接口
package cn.itheima.service;
/**
* 客户service接口
*/
public interface CustomerService {
/**
* 保存客户
*/
void saveCustomer();
/**
* 根据客户Id查询客户
*/
void findCustomerById(Integer id);
}
- 编写客户service实现类
package cn.itheima.service.impl;
import cn.itheima.service.CustomerService;
/**
* 客户service实现类
*/
public class CustomerServiceImpl implements CustomerService {
/**
* 保存客户
*/
public void saveCustomer() {
System.out.println("保存客户。");
}
/**
* 根据客户Id查询客户
*/
public void findCustomerById(Integer id) {
System.out.println("根据客户Id查询客户。客户Id:"+id);
}
}
编写通知类
这一步骤,对应开发阶段的第二件事情:抽取公共的代码,制作成通知。
package cn.itheima.advice;
/**
* 日志通知
*/
public class LogAdvice {
/**
* 记录用户操作日志
*/
public void printLog(){
System.out.println("记录用户操作日志。");
}
}
编写bean.xml
把业务层对象,和通知类对象让spring的IOC容器管理起来。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--配置客户service-->
<bean id="customerService" class="cn.itheima.service.impl.CustomerServiceImpl"></bean>
<!--配置日志通知advice-->
<bean id="logAdvice" class="cn.itheima.advice.LogAdvice"></bean>
</beans>
编写表现层对象
package cn.itheima.controller;
import cn.itheima.service.CustomerService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* 客户表现层对象
*/
public class CustomerController {
public static void main(String[] args) {
// 1.加载spring配置文件,创建spring容器
ApplicationContext context = new ClassPathXmlApplicationContext("classpath:bean.xml");
// 2.获取客户service
CustomerService customerService = (CustomerService) context.getBean("customerService");
// 3.保存客户
customerService.saveCustomer();
}
}
说明:到这里我们完成了开发阶段的前两件事情。
-
根据业务需求,编写业务代码
-
抽取公共代码,制作通知
接下来需要完成第三件事情:把业务功能与通知结合起来。
spring的AOP配置【重点】
这一步骤,对应开发阶段的第三件事情:通过配置方式,建立业务功能与通知的关系。
导入aop名称空间
配置步骤
第一步:通过aop:config声明aop配置
<!--第一步:通过aop:config声明aop配置-->
<aop:config>
</aop:config>
第二步:通过aop:aspect配置切面
<!--第一步:通过aop:config声明aop配置-->
<aop:config>
<!--第二步:通过aop:aspect配置切面,说明:
id:给切面取一个唯一标识的名称
ref:指定通知对象的引用。这里是日志通知bean的id
-->
<aop:aspect id="logAspect" ref="logAdvice">
</aop:aspect>
</aop:config>
第三步:通过aop:before配置前置通知
<!--第一步:通过aop:config声明aop配置-->
<aop:config>
<!--第二步:通过aop:aspect配置切面,说明:
id:给切面取一个唯一标识的名称
ref:指定通知对象的引用。这里是日志通知bean的id
-->
<aop:aspect id="logAspect" ref="logAdvice">
<!--第三步:通过aop:before配置前置通知,说明:
method:指定通知方法的名称
pointcut-ref:指定切入点表达式的id
-->
<aop:before method="printLog" pointcut-ref="pt1"></aop:before>
</aop:aspect>
</aop:config>
第四步:通过aop:pointcut配置切入点表达式
<aop:config>
<aop:aspect id="logAspect" ref="logAdvice">
<aop:before method="printLog" pointcut-ref="pt1"></aop:before>
<!--第四步:通过aop:pointcut配置切入点表达式,说明:
id:给切入点表达式取一个唯一标识名称
expression:指定切入点表达式
表达式组成:访问修饰符 返回值 包名称 类名称 方法名称 (参数列表)
-->
<aop:pointcut id="pt1"
expression="execution(public void cn.itheima.service.impl.CustomerServiceImpl.saveCustomer())"></aop:pointcut>
</aop:aspect>
</aop:config>
测试
切入点表达式演化【重点】
说明:切入点表达式,也叫aspectj表达式。
表达式组成部分
访问修饰符 方法返回值 包名称 类名称 方法名称 (参数列表)
表达式演化案例
创建项目
案例配置
<!--aop配置-->
<aop:config>
<aop:aspect id="logAspect" ref="logAdvice">
<aop:before method="printLog" pointcut-ref="pt1"></aop:before>
<!--切入点表达式演化,说明:
1、全匹配方式:
public void cn.itheima.service.impl.CustomerServiceImpl.saveCustomer()
2、 访问修饰符可以省略:
void cn.itheima.service.impl.CustomerServiceImpl.saveCustomer()
3、 方法返回值可以使用通配符*:
* cn.itheima.service.impl.CustomerServiceImpl.saveCustomer()
4、 包名称可以使用通配符*(有多少级包,需要多少个*):
* *.*.*.*.CustomerServiceImpl.saveCustomer()
5、 包名称可以使用..(表示当前包和子包):
* *..CustomerServiceImpl.saveCustomer()
6、 类名称可以使用通配符*:
* *..*.saveCustomer()
7、 方法名称可以使用通配符*:
* *..*.*()
8、 参数列表可以使用通配符*(此时必须要有参数) :
* *..*.*(*)
9、 参数列表可以使用..(有无参数均可):
* *..*.*(..)
10、 实际项目中的表达式案例:
* cn.itheima.service..*.*(..)
-->
<aop:pointcut id="pt1"
expression="execution(* cn.itheima.service..*.*(..))"></aop:pointcut>
</aop:aspect>
</aop:config>
常用标签说明
<aop:config>作用:声明aop配置。
<aop:aspect> 作用:配置切面。
属性:
id:唯一标识切面的名称
ref:引用通知类bean的id
<aop:pointcut> 作用:配置要增强的丰富。
属性:
id:唯一标识切入点表达式名称
expression:定义切入点表达式
<aop:before> 作用:配置前置通知
属性:
method:指定通知方法名称
pointcut:定义切入点表达式
pointcut-ref:引用切入点表达式的id
<aop:after-returning>作用:配置后置通知
属性:
method:指定通知方法名称
pointcut:定义切入点表达式
pointcut-ref:引用切入点表达式的id
<aop:after-throwing>作用:配置异常通知
属性:
method:指定通知方法名称
pointcut:定义切入点表达式
pointcut-ref:引用切入点表达式的id
<aop:after>作用:配置最终通知
属性:
method:指定通知方法名称
pointcut:定义切入点表达式
pointcut-ref:引用切入点表达式的id
<aop:around>作用:配置环绕通知
属性:
method:指定通知方法名称
pointcut:定义切入点表达式
pointcut-ref:引用切入点表达式的id
通知类型【重点】
通知类型
-
前置通知:在目标方法执行前执行
-
后置通知:在目标方法正常返回后执行。它和异常通知只能执行一个
-
异常通知:在目标方法发生异常后执行。它和后置通知只能执行一个
-
最终通知:无论目标方法正常返回,还是发生异常都会执行
-
环绕通知:综合了前面四类通知,可以手动控制通知的执行时间点和顺序
案例演示
创建项目
改造日志通知类LogAdvice,增加其它通知类型
package cn.itheima.advice;
/**
* 日志通知:通知类型
*/
public class LogAdvice {
/**
* 前置通知
*/
public void beforeLog(){
System.out.println("【前置通知】记录用户操作日志。");
}
/**
* 后置通知
*/
public void afterReturningLog(){
System.out.println("【后置通知】记录用户操作日志。");
}
/**
* 异常通知
*/
public void afterThrowingLog(){
System.out.println("【异常通知】记录用户操作日志。");
}
/**
* 最终通知
*/
public void afterLog(){
System.out.println("【最终通知】记录用户操作日志。");
}
}
配置bean.xml
<!--aop配置-->
<aop:config>
<aop:aspect id="logAspect" ref="logAdvice">
<!--前置通知-->
<aop:before method="beforeLog" pointcut-ref="pt1"></aop:before>
<!--后置通知-->
<aop:after-returning method="afterReturningLog" pointcut-ref="pt1"></aop:after-returning>
<!--异常通知-->
<aop:after-throwing method="afterThrowingLog" pointcut-ref="pt1"></aop:after-throwing>
<!--最终通知-->
<aop:after method="afterLog" pointcut-ref="pt1"></aop:after>
<aop:pointcut id="pt1"
expression="execution(* cn.itheima.service..*.*(..))"></aop:pointcut>
</aop:aspect>
</aop:config>
测试
目标方法正常返回
目标方法发生异常
环绕通知案例演示【了解】
环绕通知介绍
环绕通知,是spring框架为我们提供了手动控制通知执行时间点和顺序的一种特殊通知类型,它使用起来会更加灵活一些。
改造日志通知类LogAdvice,增加环绕通知
/**
* 环绕通知:
* 1.它是spring框架为我们提供了手动控制通知执行时间点和顺序的一种特殊通知类型
* 原理分析:
* 2.spring框架提供了ProceedingJoinPoint接口,作为环绕通知的参数。在环绕通知
* 执行的时候,spring框架会提供实例化对象,我们直接使用即可。该接口中提供了
* 两个方法:
* getArgs:获取参数列表
* proceed:相当于反射中的invoke方法
*
*/
public void aroundLog(ProceedingJoinPoint pjp){
// 前置通知
System.out.println("【环绕通知-前置通知】记录用户操作日志。");
try {
// 获取参数列表
Object[] args = pjp.getArgs();
// 反射调用目标方法
Object retV = pjp.proceed(args);
// 后置通知
System.out.println("【环绕通知-后置通知】记录用户操作日志。");
} catch (Throwable throwable) {
throwable.printStackTrace();
// 异常通知
System.out.println("【环绕通知-异常通知】记录用户操作日志。");
}
// 最终通知
System.out.println("【环绕通知-最终通知】记录用户操作日志。");
}
配置bean.xml
<!--aop配置-->
<aop:config>
<aop:aspect id="logAspect" ref="logAdvice">
<!--环绕通知-->
<aop:around method="aroundLog" pointcut-ref="pt1"></aop:around>
<aop:pointcut id="pt1"
expression="execution(* cn.itheima.service..*.*(..))"></aop:pointcut>
</aop:aspect>
</aop:config>
测试
目标方法正常返回
目标方法发生异常
基于xml和注解的AOP配置【掌握】
创建项目
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/898273a5c014bd0ec65a98f123cc462d.png)
改造客户service实现类,使用注解方式配置
package cn.itheima.service.impl;
import cn.itheima.service.CustomerService;
import org.springframework.stereotype.Service;
/**
* 客户service实现类
*/
@Service("customerService")
public class CustomerServiceImpl implements CustomerService {
/**
* 保存客户
*/
public void saveCustomer() {
System.out.println("保存客户。");
int i=1/0;
}
/**
* 根据客户Id查询客户
*/
public void findCustomerById(Integer id) {
System.out.println("根据客户Id查询客户。客户Id:"+id);
}
}
改造通知类LogAdvice,使用注解方式配置
注解说明:
-
@Aspect:
作用:声明当前类是一个切面类,相当于xml中aop:aspect标签
-
@Before:
作用:配置前置通知
-
@AfterReturning:
作用:配置后置通知
-
@AfterThrowing:
作用:配置异常通知
-
@After:
作用:配置最终通知
-
@Pointcut:
作用:配置切入点表达式
package cn.itheima.advice;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Component("logAdvice")
@Aspect
public class LogAdvice {
/**
* 切入点表达式
*/
@Pointcut("execution(* cn.itheima.service..*.*(..))")
public void pt1(){}
/**
* 前置通知
* @Before("execution(* cn.itheima.service..*.*(..))")
*/
@Before("pt1()")
public void beforeLog(){
System.out.println("【前置通知】记录用户操作日志。");
}
/**
* 后置通知
*/
@AfterReturning("pt1()")
public void afterReturningLog(){
System.out.println("【后置通知】记录用户操作日志。");
}
/**
* 异常通知
*/
@AfterThrowing("pt1()")
public void afterThrowingLog(){
System.out.println("【异常通知】记录用户操作日志。");
}
/**
* 最终通知
*/
@After("pt1()")
public void afterLog(){
System.out.println("【最终通知】记录用户操作日志。");
}
/**
* 环绕通知:
* 1.它是spring框架为我们提供了手动控制通知执行时间点和顺序的一种特殊通知类型
* 原理分析:
* 2.spring框架提供了ProceedingJoinPoint接口,作为环绕通知的参数。在环绕通知
* 执行的时候,spring框架会提供实例化对象,我们直接使用即可。该接口中提供了
* 两个方法:
* getArgs:获取参数列表
* proceed:相当于反射中的invoke方法
*
*/
@Around("pt1()")
public void aroundLog(ProceedingJoinPoint pjp){
// 前置通知
System.out.println("【环绕通知-前置通知】记录用户操作日志。");
try {
// 获取参数列表
Object[] args = pjp.getArgs();
// 反射调用目标方法
Object retV = pjp.proceed(args);
// 后置通知
System.out.println("【环绕通知-后置通知】记录用户操作日志。");
} catch (Throwable throwable) {
throwable.printStackTrace();
// 异常通知
System.out.println("【环绕通知-异常通知】记录用户操作日志。");
}
// 最终通知
System.out.println("【环绕通知-最终通知】记录用户操作日志。");
}
}
配置bean.xml
<?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:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!--配置扫描包-->
<context:component-scan base-package="cn."></context:component-scan>
</beans>
测试
问题:虽然使用注解方式配置好了客户业务层对象、日志通知类对象,以及aop相关的配置。并且在bean.xml中也配置了扫描包。但是当我们执行保存客户操作,却没有得到增强???
问题的原因:还需要启用spring对象注解aop的支持。它是一个关键步骤。
启用spring对注解AOP的支持【关键步骤】
<?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:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!--配置扫描包-->
<context:component-scan base-package="cn.itheima"></context:component-scan>
<!--启用spring对注解aop支持-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
再次测试
纯注解的AOP配置【了解】
说明:我们使用纯注解来配置aop。根据我们第二天课程内容,spring的纯注解配置,即是要编写一个配置类,来代替bean.xml文件。这个版本大家了解即可。在企业项目中使用较少。
创建项目
编写spring配置类
package cn.itheima.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
/**
* spring配置类
*
* 注解说明:
* 1.@Configuration:声明当前类是spring配置类
* 2.@ComponentScan:配置扫描包,相当于xml中context:component-scan标签
* 3.@EnableAspectJAutoProxy:启用spring对注解AOP的支持,相当于xml中aop:aspectj-autoproxy标签
*/
@Configuration
@ComponentScan(value = {"cn.itheima"})
@EnableAspectJAutoProxy
public class SpringConfiguration {
}
测试
小结
AOP常用注解
1、直接在切面类上定义通知的功能,并配置相关的注解
2、在配置类中设置@EnableAspectJAutoProxy注解
package com.log;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class Log {
@Pointcut("execution(* com.server..*.*(..))")
void pt() {
}
@Before("pt()")
void beforeFun() {
System.out.println("开始记录日志");
}
@After("pt()")
void afterFun() {
System.out.println("结束记录日志");
}
@AfterThrowing("pt()")
void haserror() {
System.out.println("发生故障");
}
@AfterReturning("pt()")
void whenReturning() {
System.out.println("返回成功");
}
@Around("pt()")
Object around(ProceedingJoinPoint pjp) {
System.out.println("开始记录日志2");
Object retV = null;
try {
// 获取参数列表
Object[] args = pjp.getArgs();
// 反射调用目标方法
retV = pjp.proceed(args);
// 后置通知
System.out.println("结束记录日志2");
} catch (Throwable throwable) {
throwable.printStackTrace();
// 异常通知
System.out.println("发生故障2");
}
System.out.println("最终通知");
return retV;
}
}
AOP常用XML标签
<!--切面类-->
<bean id="log" class="com.log.Log"/>
<!--配置切面-->
<aop:config>
<aop:aspect id="logAspect" ref="log">
<aop:pointcut id="pt1" expression="execution(* com.server..*.*(..))"/>
<aop:around method="around" pointcut-ref="pt1"/>
<aop:before method="beforeFun" pointcut-ref="pt1"/>
<aop:after method="afterFun" pointcut-ref="pt1"/>
<aop:after-returning method="whenReturning" pointcut-ref="pt1"/>
<aop:after-throwing method="haserror" pointcut-ref="pt1"/>
</aop:aspect>
</aop:config>
注意around通知对应函数的返回值要为Object.
package com.log;
import org.aspectj.lang.ProceedingJoinPoint;
public class Log {
void pt() {
}
void beforeFun() {
System.out.println("开始记录日志");
}
void afterFun() {
System.out.println("结束记录日志");
}
void haserror() {
System.out.println("发生故障");
}
void whenReturning() {
System.out.println("返回成功");
}
Object around(ProceedingJoinPoint pjp) {
System.out.println("开始记录日志2");
Object retV = null;
try {
// 获取参数列表
Object[] args = pjp.getArgs();
// 反射调用目标方法
retV = pjp.proceed(args);
// 后置通知
System.out.println("结束记录日志2");
} catch (Throwable throwable) {
throwable.printStackTrace();
// 异常通知
System.out.println("发生故障2");
}
System.out.println("最终通知");
return retV;
}
}