Spring AOP
代理设计模式
代理设计模式的优点:将通用性的工作都交给代理对象完成,被代理对象只需关乎自己的核心业务。
静态代理
静态代理,代理类只能够为特定的类生产代理对象,不能代理任意类
使用代理的好处
- 被代理类中只用关注核心业务的实现,将通用的管理型逻辑(事务管理、日志管理)和业务逻辑分离
- 将通用的代码放在代理类中实现,提供了代码的复用性
- 通过在代理类添加业务逻辑,实现对原有业务逻辑的扩展(增强)
动态代理
动态代理,几乎可以为所有的类产生代理对象
动态代理的实现方式有两种:
JDK动态代理
CGLib动态代理
JDK动态代理
package com.qfedu.dao;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* @author Helen
* @version 1.0
* @createTime 2022/1/9 17:36
* @Description JDK动态代理:是通过被代理对象实现的接口产生其代理对象的
* 1. 创建一个类,实现InvocationHandler接口,实现invoke方法
* 2.在类中定义一个Object类型的变量,并提供这个变量的有参构造器,用于将被代理对象传递进来
* 3.创建getProxy方法,用于创建并返回代理对象
*/
public class JdkDynamicProxy implements InvocationHandler {
/**
* 被代理对象
*/
private Object object;
public JdkDynamicProxy(Object object) {
this.object = object;
}
/**
* 产生代理对象,返回代理对象
*/
public Object getProxy() {
/**1.获取被代理对象的类加载器*/
ClassLoader classLoader = object.getClass().getClassLoader();
/**2.获取被代理对象的类实现的接口*/
Class<?>[] interfaces = object.getClass().getInterfaces();
/**3.产生代理对象(通过被代理对象的类加载器及实现的接口)
* 第一个参数:被代理对象的类加载器
* 第二个参数:被代理对象实现的接口
* 第三个参数:使用产生代理对象调用方法时,用于拦截方法执行的处理器
* */
Object obj = Proxy.newProxyInstance(classLoader, interfaces, this);
return obj;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("----------------拦截");
begin();
/**执行method方法(传递进来的方法)*/
Object obj = method.invoke(object,args);
// System.out.println(method);
commit();
return obj;
}
public void begin() {
System.out.println("-----开启事务");
}
public void commit() {
System.out.println("-----提交事务");
}
}
测试代码
package com.qfedu.dao;
/**
* @author Helen
* @version 1.0
* @createTime 2022/1/9 17:46
* @Description
*/
public class JdkDynamicProxyTest{
public static void main(String[] args) {
/**被代理对象*/
BookDAOImpl bookDAO=new BookDAOImpl();
StudentDAOImpl studentDAO=new StudentDAOImpl();
/**创建动态代理对象,并将被代理对象传递到代理类中赋值给obj*/
JdkDynamicProxy jdkDynamicProxy=new JdkDynamicProxy(bookDAO);
/**proxy就是产生的代理对象:产生的代理对象可以强转成被代理对象实现的接口类型*/
GeneralDAO proxy=(GeneralDAO)jdkDynamicProxy.getProxy();
/**使用代理对象调用方法,不会直接进入被代理类,而是进入到创建代理对象时指定的InvocationHandler类中的invoke方法,
*调用的方法作为一个Method参数,传递给了invoke方法
*/
proxy.delete();
}
}
测试结果
CGLib动态代理
由于JDK动态代理是通过被代理类实现的接口来创建代理对象的,因此JDK动态代理只能实现了接口的类的对象。如果一个类没有实现任何接口,该如何产生代理对象呢?
CGLib动态代理,是通过创建被代理类的子类来创建代理对象的,因此即使没有实现任何接口的类也可以通过CGLib产生代理对象
CGLib动态代理不能为final
类创建代理对象
- 添加CGLib的依赖
<!-- https://mvnrepository.com/artifact/cglib/cglib -->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
package com.qfedu.dao;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
* @author Helen
* @version 1.0
* @createTime 2022/1/9 20:52
* @Description
* 1.添加cglib依赖
* 2. 创建一个类,实现MethodInterceptor,同时实现接口intercept方法
* 3. 在类中定义一个Object类型的变量,并提供这个变量的有参构造器,用于传递被代理对象
* 4. 定义getProxy方法创建并返回代理对象(代理对象是通过创建被代理类的子类来创建的)
*/
public class CGLibDynamicProxy implements MethodInterceptor {
private Object object;
public CGLibDynamicProxy(Object object){
this.object=object;
}
public Object getProxy(){
Enhancer enhancer=new Enhancer();
/**设置父类*/
enhancer.setSuperclass(object.getClass());
/***/
enhancer.setCallback(this);
return enhancer.create();
}
public void begin() {
System.out.println("-----开启事务");
}
public void commit() {
System.out.println("-----提交事务");
}
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("cglib拦截");
begin();
/**通过反射调用被代理类的方法*/
Object obj = method.invoke(object,objects);
commit();
return obj;
}
}
测试
package com.qfedu.dao;
/**
* @author Helen
* @version 1.0
* @createTime 2022/1/9 21:09
* @Description
*/
public class CGLibDynamicProxyTest {
public static void main(String[] args) {
/**创建被代理对象*/
BookDAOImpl bookDAO=new BookDAOImpl();
/**通过cglib动态代理类创建代理对象*/
CGLibDynamicProxy cgLibDynamicProxy=new CGLibDynamicProxy(bookDAO);
/**代理对象实际上是被代理对象子类,因此代理对象可直接强转为被代理类类型*/
BookDAOImpl obj= (BookDAOImpl) cgLibDynamicProxy.getProxy();
/**使用对象调用方法,实际上并没有执行这个方法,而是执行了代理类中的intercept犯法,将当前调用的方法以及方法中的参数
* 传递到intercept方法
*/
obj.delete();
}
}
Spring AOP
AOP概念
Aspect Oriented Programming
面向切面编程,是一种利用“横切”
的技术(底层实现就是动态代理),对原有的业务逻辑进行拦截,并且可以在这个拦截的横切面上添加特定的业务逻辑,对原有的业务进行增强。
基于动态实现在不改变原有业务的情况下
框架部署
框架依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.19.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.19.RELEASE</version>
</dependency>
创建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"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
</beans>
AOP-基于XML
在DAO的方法添加开启事务和提交事务的逻辑
创建一个类,定义要添加的业务逻辑
public class TxManager {
public void begin(){
System.out.println("------开启事务");
}
public void commit(){
System.out.println("-----提交事务");
}
}
配置aop
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
https://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">
<context:annotation-config/>
<context:component-scan base-package="com.qfedu"/>
<aop:config>
<!--声明切入点-->
<aop:pointcut id="bookUpdate" expression="execution(* com.qfedu.dao.*.*(..))"/>
<!--声明txManager为切面类-->
<aop:aspect ref="txManager">
<!--通知-->
<aop:before method="begin" pointcut-ref="bookUpdate"/>
<aop:after method="commit" pointcut-ref="bookUpdate"/>
</aop:aspect>
</aop:config>
</beans>
package com.qfedu.utils;
import org.springframework.stereotype.Component;
/**
* @author Helen
* @version 1.0
* @createTime 2022/1/9 22:42
* @Description
*/
@Component
public class TxManager {
public void begin(){
System.out.println("------开启事务");
}
public void commit(){
System.out.println("-----提交事务");
}
}
package com.qfedu.dao;
import org.springframework.stereotype.Component;
/**
* @author Helen
* @version 1.0
* @createTime 2022/1/9 16:43
* @Description
*/
@Component(value = "bookDAO")
public class BookDAOImpl{
public void insert() {
System.out.println("insert book");
}
public void delete(int id) {
System.out.println("delete book id = "+id);
}
public void update() {
System.out.println("update book");
}
}
测试代码
package com.qfedu.test;
import com.qfedu.dao.BookDAOImpl;
import com.qfedu.dao.StudentDAOImpl;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* @author Helen
* @version 1.0
* @createTime 2022/1/9 22:33
* @Description
*/
public class TestCase {
public static void main(String[] args) {
/**通过Spring容器获取BookDAOImpl的对象,并调用方法*/
ClassPathXmlApplicationContext context=new ClassPathXmlApplicationContext("applicationContext.xml");
BookDAOImpl bookDAO=context.getBean("bookDAO",BookDAOImpl.class);
bookDAO.insert();
bookDAO.delete(2);
}
}
测试结果
AOP开发步骤
- 创建切面类,在切面类定义切点方法
- 将切面类配置给Spring容器
- 声明切入点
- 配置AOP的通知策略
切入点声明
各种切入点声明方式
例如定义切入点表达式execution (* com.qfedu.dao..*. *(..))
execution()是最常用的切点函数,其语法如下所示:
整个表达式可以分为五个部分:
1、execution(): 表达式主体。
2、第一个*
号:表示返回类型, *
号表示所有的类型。
3、包名:表示需要拦截的包名,后面的两个句点..
表示当前包和当前包的所有子包,com.sample.service.impl包、子孙包下所有类的方法。
4、第二个*
号:表示类名,*
号表示所有的类。
5、*(..)
:最后这个星号表示方法名,*
号表示所有的方法,后面括弧里面表示方法的参数,两个句点表示任何参数
<aop:config>
<!--使用aop:pointcut标签声明切入点:切入点可以是一个方法-->
<aop:pointcut id="bookUpdate" expression="execution(* com.qfedu.dao.*.*(..))"/>
<!--BookDAOImpl类中所有无参数无返回值的方法-->
<aop:pointcut id="book_pc1" expression="execution(void com.qfedu.dao.BookDAOImpl.*())"/>
<!--BookDAOImpl类中所有无返回值的方法-->
<aop:pointcut id="book_pc2" expression="execution(void com.qfedu.dao.BookDAOImpl.*(..))"/>
<!--BookDAOImpl类中所有无参数的方法-->
<aop:pointcut id="book_p3" expression="execution(* com.qfedu.dao.BookDAOImpl.*())"/>
<!--BookDAOImpl类中所有方法-->
<aop:pointcut id="book_p4" expression="execution(* com.qfedu.dao.BookDAOImpl.*(..))"/>
<!--dao包中所有类中的所有方法-->
<aop:pointcut id="p5" expression="execution(* com.qfedu.dao.*.*(..))"/>
<!--dao包中所有类中的insert方法-->
<aop:pointcut id="p6" expression="execution(* com.qfedu.dao.*.insert(..))"/>
<!--dao包中所有类中的方法-->
<aop:pointcut id="p7" expression="execution(* *(..))"/>
<!--声明txManager为切面类-->
<aop:aspect ref="txManager">
<!--通知-->
<aop:before method="begin" pointcut-ref="p7"/>
<aop:after method="commit" pointcut-ref="p7"/>
</aop:aspect>
<aop:aspect ref="logManager">
<!-- <aop:before method="printLog" pointcut-ref=""/>-->
</aop:aspect>
</aop:config>
AOP使用注意事项
/**通过Spring容器获取BookDAOImpl的对象,并调用方法*/
ClassPathXmlApplicationContext context=new ClassPathXmlApplicationContext("applicationContext.xml");
BookDAOImpl bookDAO=context.getBean("bookDAO",BookDAOImpl.class);
bookDAO.insert();
bookDAO.delete(2);
/**如果要使用Spring aop面向切面编程,调用切入点方法的对象必须通过Spring容器获取
* 如果一个类中的方法被声明为切入点并且织入切点之后,通过Spring容器获取对象,实则获取到的是一个代理对象
* 如果一个类中的方法没有被声明为切入点,通过Spring容器获取的就是这个类真实创建的对象
* */
BookService bookService=context.getBean("bookService",BookService.class);
bookService.addBook();
AOP通知策略
AOP通知策略:就是声明将切面类中的切点方法如何织入到切入点
- before
- after
- after-throwing
- after-returning
- around
定义切面类
package com.qfedu.utils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.stereotype.Component;
/**
* @author Helen
* @version 1.0
* @createTime 2022/1/12 18:04
* @Description
*/
@Component
public class MyAspect {
public void method1(){
System.out.println("method1");
}
public void method2(){
System.out.println("method2");
}
public void method3(){
System.out.println("method3");
}
public void method4(){
System.out.println("method4");
}
/**
*环绕通知的切点方法必须遵守如下规则:
* 1. 必须带有一个ProceedingJoinPoint类型的参数,
* 2. 必须有Object类型的返回值
* 3.在前后增强的业务逻辑之间执行 Object o=joinPoint.proceed();
* 4. 方法最后返回o
* */
public Object method5(ProceedingJoinPoint joinPoint) throws Throwable {
/**此行代码的执行,表示切入点代码的执行*/
Object o=joinPoint.proceed();
System.out.println("环绕通知");
System.out.println("joinPoint="+joinPoint);
System.out.println("o="+o);
System.out.println("method5");
return o;
}
}
配置切面类
<?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
https://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">
<context:annotation-config/>
<context:component-scan base-package="com.qfedu"/>
<aop:config>
<!--dao包中所有类中的insert方法-->
<aop:pointcut id="p6" expression="execution(* com.qfedu.dao.*.insert(..))"/>
<!--声明txManager为切面类-->
<aop:aspect ref="myAspect">
<!--aop:before 前置通知,指定切入点之前-->
<aop:before method="method1" pointcut-ref="p6"/>
<!--aop:after 后置通知,指定切入点之后-->
<aop:after method="method2" pointcut-ref="p6"/>
<!--aop:after-throwing异常通知,切入点方法抛出异常之后-->
<aop:after-throwing method="method3" pointcut-ref="p6"/>
<!--after-returning:方法返回值返回之后,对于一个java方法而言,return 返回值
也是方法的一部分,因此,“方法返回值返回之后”和“方法执行结束”是同一个时间点,所以,
after和after-returning根据配置的顺序决定执行的顺序
-->
<aop:after-returning method="method4" pointcut-ref="p6"/>
<aop:around method="method5" pointcut-ref="p6"/>
<!-- <aop:around method="method2" pointcut-ref="p6"/>-->
</aop:aspect>
</aop:config>
</beans>
测试
package com.qfedu.test;
import com.qfedu.dao.BookDAOImpl;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* @author Helen
* @version 1.0
* @createTime 2022/1/12 18:11
* @Description
*/
public class TestCase2 {
public static void main(String[] args) {
ApplicationContext context=new ClassPathXmlApplicationContext("applicationContext.xml");
BookDAOImpl bookDAO=context.getBean("bookDAO",BookDAOImpl.class);
bookDAO.insert();
}
}
测试结果
Spring AOP 注解配置
- 创建Maven工厂
- 添加Spring依赖(context,aspects)
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.19.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.19.RELEASE</version>
</dependency>
</dependencies>
- Spring配置文件
<?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
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<context:annotation-config/>
<context:component-scan base-package="com.qfedu"/>
<!--基于注解配置的AOP代理-->
<aop:aspectj-autoproxy proxy-target-class="true"/>
</beans>
- AOP注解配置案例
package com.qfedu.dao;
import org.springframework.stereotype.Component;
/**
* @author Helen
* @version 1.0
* @createTime 2022/1/12 19:33
* @Description
*/
@Component(value = "userDAO")
public class UserDAOImpl {
public void insert(){
System.out.println("insert-------user");
}
public void delete(int id){
System.out.println("delete id="+id);
}
}
package com.qfedu.utils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
/**
* @author Helen
* @version 1.0
* @createTime 2022/1/12 19:41
* @Description 切面类
*/
@Component
@Aspect
public class TransactionManager {
/**
* 声明切入点
* p1:切入点名称
*/
@Pointcut("execution(* com.qfedu.dao.*.*(..))")
public void p1(){
}
/**
*@Before: 前置通知
* begin:前置通知名称
* p1():切入点方法
*/
@Before("p1()")
public void begin(){
System.out.println("----开启事务");
}
/**
* @After: 后置通知
*/
@After("p1()")
public void commit(){
System.out.println("----提交");
}
/**
* 环绕通知
* @param joinPoint
* @return
* @throws Throwable
*/
@Around("p1()")
public Object printExecuteTime(ProceedingJoinPoint joinPoint) throws Throwable {
long time1 = System.currentTimeMillis();
Object o =joinPoint.proceed();
System.out.println("joinPoint="+joinPoint);
System.out.println(o);
long time2=System.currentTimeMillis();
System.out.println("time="+(time2-time1));
return o;
}
}
注意:
注解使用虽然方便,但是只能在源码上添加注解,因此我们的自定义类提倡使用注解配置,如果使用到第三方提供的类则需要通过xml配置形式完成配置。
测试代码
package com.qfedu.test;
import com.qfedu.dao.UserDAOImpl;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* @author Helen
* @version 1.0
* @createTime 2022/1/12 19:34
* @Description
*/
public class TestCase {
public static void main(String[] args) {
ApplicationContext context=new ClassPathXmlApplicationContext("applicationContext.xml");
UserDAOImpl userDAO= context.getBean("userDAO", UserDAOImpl.class);
userDAO.insert();
userDAO.delete(2);
}
}
测试结果