JAVA框架-Spring02(AOP)
AOP概念
AOP为Aspect Oriented Programming的缩写,翻译为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也 是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分 进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效 率。
一些相关的术语:(示例:下面案例中的三个方法)
- 切点(pointcut)
切点指的是要被扩展(增加了功能)的内容,包括方法或属性(joinpoint)
示例:案例中的两个增加了功能的方法
- 通知(adivce)
通知指的是要在切点上增加的功能
按照执行时机不同分为:
前置,后置,异常,最终,环绕,引介
引介通知指的是在不修改类代码的前提下,为类增加方法或属性(了解即可非重点)
示例:案例中的输出执行时间功能
- 目标(target)
目标就是要应用通知的对象,即要被增强的对象
示例:案例中的userDao
- 织入(weaving)
织入是一个动词,描述的是将扩展功能应用到target的这个过程
示例:案例中修改源代码的过程
- 代理(proxy)
Spring是使用代理来完成AOP,对某个对象增强后就得到一个代理对象;
Spring AOP的整个过程就是对target应用advice最后产生proxy,我们最后使用的都是proxy对象; 狸猫换太子,偷梁换柱;
- 切面(aspect)
是切入点和通知的结合切面,是一个抽象概念; 一个切面指的是所有应用了同一个通知的切入点的集合
示例:案例中的save 和 delete方法共同组成一个切面
Emmmmm......简单说,就是有一些方法,我们希望能够批量的添加一些代码进去.....
没有使用AOP之前
在某个的Dao层如UserDao,存在以下几个方法:
public class UserDao{
public void save(){
System.out.println("save sql");
}
public void delete(){
System.out.println("delete sql");
}
public void update(){
System.out.println("update sql");
}
}
在第一个版本中,已经实现了程序的实际功能,但是后来发现数据库操作出现瓶颈,这时领导说要对这些方法进行执行时间统计,并输出日志分析问题;那么我们只能这样做:
public class UserDao{
public void save(){
//获取类名和方法名
String className = Thread.currentThread().getStackTrace()[1].getClassName();
String methodName = Thread.currentThread().getStackTrace()[1].getMethodName();
//开始时间
long startTime = new Date().getTime();
//原逻辑
System.out.println("save sql");
//耗时
long runTime = new Date().getTime() - startTime;
System.out.printf("info [class:%s method:%s runtime:%s]",className,methodName,runTime);
}
public void delete(){
String className = Thread.currentThread().getStackTrace()[1].getClassName();
String methodName = Thread.currentThread().getStackTrace()[1].getMethodName();
long startTime = new Date().getTime();
System.out.println("delete sql");
long runTime = new Date().getTime() - startTime;
System.out.printf("info [class:%s method:%s runtime:%s]",className,methodName,runTime);
}
public void update(){
System.out.println("update sql");
}
public static void main(String[] args) {
new UserDao().save();
}
}
这样做的缺点
需求实现了,但是上述代码存在以下问题:
- 1.修改了源代码(违反了OCP,违反了对修改封闭,但满足调用方式不变)
- 2.大量重复代码
更好的方法呢?
我们先不考虑如何解决这些问题,其实AOP之所以出现就是因为,我们需要对一些已经存在的方法进行功能扩展,但是又不能通过修改源代码或改变调用方式的手段来解决
反过来说就是要在保证不修改源代码以及调用方式不变的情况下为原本的方法增加功能
而由于需要扩展的方法有很多,于是把这些方法称作一个切面,即切面就是一系列需要扩展功能的方法的集合
所以我们如果使用AOP的话,我们的目的应该如下:
将日志记录,性能统计,安全控制,事务处理,异常处理等重复代码从业务逻辑代码中划分 出来,通过对这些行为的分离,我们希望可以将它们独立到非业务逻辑的方法中,进而改变这些行为的时候不会影响业务逻辑的代码。
我们可以利用上图的思路来实现,所以关键的点就是借用代理对象来间接去实现该方法,达到一定程度的代码复用。
dao接口:
package dao;
/**
* Created by Jeason Luna on 2020/6/25 11:09
*/
public interface DaoInerface {
public void save();
public void delete();
public void update();
}
userDao:
package dao;
/**
* Created by Jeason Luna on 2020/6/25 11:09
*/
public class UserDao implements DaoInerface{
public void save(){
System.out.println("save sql");
}
public void delete(){
System.out.println("delete sql");
}
public void update(){
System.out.println("update sql");
}
}
代理实现类:
package dao;
import java.util.Date;
/**
* Created by Jeason Luna on 2020/6/25 11:12
*/
public class MyProxy implements DaoInerface{
private DaoInerface target;
public MyProxy(DaoInerface target) {
this.target = target;
}
@Override
public void save() {
long startTime = new Date().getTime();
target.save();
System.out.println( "任务耗时:"+ (new Date().getTime() - startTime) );
}
@Override
public void delete() {
long startTime = new Date().getTime();
target.delete();
System.out.println( "任务耗时:"+ (new Date().getTime() - startTime) );
}
@Override
public void update() {
long startTime = new Date().getTime();
target.update();
System.out.println( "任务耗时:"+ (new Date().getTime() - startTime) );
}
}
测试代码:
import dao.DaoInerface;
import dao.MyProxy;
import dao.UserDao;
import org.junit.Test;
/**
* Created by Jeason Luna on 2020/6/25 11:19
*/
public class MyTest {
@Test
public void test1(){
DaoInerface userDao = new MyProxy( new UserDao() );
userDao.update();
userDao.save();
}
}
这样就实现了我们的需求。
但是还是有很多重复的代码啊。
emmmmmm.......
下面介绍三种实现上述功能的更简单的方法
JAVA官方提供的动态代理解决方案
代理类:
package dao;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Date;
/**
* Created by Jeason Luna on 2020/6/25 11:29
*/
public class DynamicProxy implements InvocationHandler{
private DaoInerface target;
public DynamicProxy(DaoInerface target) {
this.target = target;
}
public Object getProxy(){
Object o = Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), (InvocationHandler) this);
return o;
}
//proxy 代理对象
//methed 要执行的方法
//object 调用方传递的参数列表
@Override
public Object invoke(Object proxy, Method method, Object[] objects) throws Throwable {
//当调用代理对象是,就会自动执行该方法
long time = new Date().getTime();
Object invoke = method.invoke(target, objects);
long end = new Date().getTime();
System.out.println("代理对象的任务耗时:"+ (time - end));
return invoke;
}
}
测试:
@Test
public void test2(){
DaoInerface userDao = (DaoInerface) new DynamicProxy(new UserDao()).getProxy();
userDao.update();
userDao.save();
}
这样代码就少多了。
注意:
1.动态代理,要求被代理的target对象必须实现了某个接口,且仅能代理接口中声明的方法,这给开发带来了一些限制,当target不是某接口实现类时,则无法使用动态代理,CGLib则可以解决这个问题
2.被拦截的方法包括接口中声明的方法以及代理对象和目标对象都有的方法如:toString
3.对代理对象执行这些方法将造成死循环
CGLib解决方案
上面的方法要求必须存在接口,下面演示CGLib的实现方法,原理是实现一个子类来实现的。
maven依赖:
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.2.5</version>
</dependency>
代理类:
package dao;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Date;
/**
* Created by Jeason Luna on 2020/6/25 11:29
*/
public class CGLibProxy implements MethodInterceptor {
private DaoInerface target;
public CGLibProxy(DaoInerface target) {
this.target = target;
}
public Object getProxy(){
//增强器 CGLIB中
Enhancer enhancer = new Enhancer();
//设置代理类的父类是谁
enhancer.setSuperclass( target.getClass() );
//
enhancer.setCallback(this);
return enhancer.create();
}
//proxy 代理对象
//method 外界要执行的方法
//objects 传递的参数
//methodProxy 方法代理对象
//Throwable 用于执行方法
@Override
public Object intercept(Object proxy, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
//添加的额外逻辑
System.out.println( new Date() + "method: " + method.getName() );
//调用原始的方法
Object o = methodProxy.invokeSuper(proxy, objects);
return o;
}
}
测试:
@Test
public void test3(){
DaoInerface userDao = (DaoInerface) new CGLibProxy( new UserDao()).getProxy();
userDao.update();
userDao.save();
}
注意:
1.CGLib可以拦截代理目标对象的所有方法
2.CGLib采用的是产生一个继承目标类的代理类方式产生代理对象,所以如果类被final修饰将无法使用CGLib
Spring中的AOP
Spring在运行期,可以自动生成动态代理对象,不需要特殊的编译器,Spring AOP的底层就是通过JDK动态代理和CGLib动态代理技术 为目标Bean执行横向织入。并且Spring会自动选择代理方式
1.若目标对象实现了若干接口,spring使用JDK的java.lang.reflect.Proxy类代理。
2.若目标对象没有实现任何接口,spring使用CGLIB库生成目标对象的子类。
首先我们为了在Junit测试环境中能够拿到Spring容器中的内容,我们需要引入一个测试包
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.0.9.RELEASE</version>
</dependency>
为了使Spring生成对应的Bean,写配置文件
<?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:c="http://www.springframework.org/schema/c"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean class="dao.UserDao" />
</beans>
随后,我们在测试类的上面加上相应的注解:
import dao.*;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* Created by Jeason Luna on 2020/6/25 11:19
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration( "classpath:applicationContext.xml" )
public class MyTest {
@Autowired
UserDao userDao;
@Test
public void test4(){
userDao.save();
}
}
这样,我们就可以在测试环境下使用Spring了。
Spring在运行期,可以自动生成动态代理对象,不需要特殊的编译器,Spring AOP的底层就是通过JDK动态代理和CGLib动态代理技术 为目标Bean执行横向织入。并且Spring会自动选择代理方式
1.若目标对象实现了若干接口,spring使用JDK的java.lang.reflect.Proxy类代理。
2.若目标对象没有实现任何接口,spring使用CGLIB库生成目标对象的子类。
Spring通知类型
-
前置
org.springframework.aop.MethodBeforeAdvice
用于在原始方法执行前的预处理 -
后置
org.springframework.aop.AfterReturningAdvice
用于在原始方法执行后的后处理 -
环绕
org.aopalliance.intercept.MethodInterceptor
这个名字不知道谁给起的,其实不算是通知,而是叫拦截器,在这里我们可以阻止原始方法的执行,而其他通知做不到 -
异常
org.springframework.aop.ThrowsAdvice
用于在原始方法抛出异常时处理 -
引介
org.springframework.aop.IntroductionInterceptor
在目标类中添加一些新的方法和属性(非重点)
在上面的案例中继续,我们在建立一个通知类:
package advice;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import org.springframework.aop.AfterReturningAdvice;
import org.springframework.aop.MethodBeforeAdvice;
import java.lang.reflect.Method;
/**
* Created by Jeason Luna on 2020/6/25 23:37
*/
public class MyAdvice implements MethodBeforeAdvice, AfterReturningAdvice, MethodInterceptor {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
return null;
}
@Override
public void afterReturning(Object o, Method method, Object[] objects, Object o1) throws Throwable {
System.out.println( method.getName() +"执行后");
}
@Override
public void before(Method method, Object[] objects, Object o) throws Throwable {
System.out.println( method.getName() +"执行前");
}
@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
System.out.println("环绕前....");
Object result = methodInvocation.proceed(); //执行原始方法
System.out.println("环绕后....");
return result;
}
}
配置文件如下:
<!-- 目标-->
<bean id="UserDao" class="dao.UserDao" />
<!-- 通知-->
<bean id = "advice" class="advice.MyAdvice"/>
<!--创建代理对象 这种方式会把所有对象全部增强-->
<bean id="proxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<!-- 指定通知-->
<property name="interceptorNames" value="advice"/>
<property name="target" ref="UserDao"/>
</bean>
此时的目录结构如图:
测试代码:
import dao.*;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* Created by Jeason Luna on 2020/6/25 11:19
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration( "classpath:applicationContext.xml" )
public class MyTest {
@Qualifier("proxy")
@Autowired
private DaoInerface userDao;
@Test
public void test4(){
userDao.save();
userDao.update();
}
}
这样我们就在Spring中完成了AOP
切入点切面使用(PointAdvisor)
上面的方法可以看出把所有的该对象的方法都进行了增强,如果我们仅仅想增强该对象的某几个方法应该怎么操作呢?
就需要使用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:c="http://www.springframework.org/schema/c"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 目标-->
<bean id="UserDao" class="dao.UserDao" />
<!-- 通知-->
<bean id = "advice" class="advice.MyAdvice"/>
<!-- 希望能够对方法加以区分,仅仅对目标对象的部分方法进行增强-->
<bean id="advisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
<!-- 编写表达式匹配方法名 (方法的完整名称, 包名+类名+方法名)-->
<property name="patterns" value="dao.UserDao.save"/>
<!--也可以指定多个正则表达式 多个表达式用逗号隔开即可-->
<!-- <property name="patterns" value=".*save,.*update"/> -->
<!--指定要应用的通知-->
<property name="advice" ref="advice"/>
</bean>
<bean id="proxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<!--指定目标-->
<property name="target" ref="UserDao"/>
<!--指向Pointcut切入点-->
<property name="interceptorNames" value="advisor"/>
</bean>
</beans>
这样我们就可以仅仅增强save()
方法了
自动生成代理
如果每个Bean都需要配置代理Bean的话,开发维护的工作量将是巨大的;
自动生成代理有三种方式
- 根据BeanName来查找目标对象并且其生成代理
- 根据切面信息来查找目标对象并且其生成代理
- 通过AspectJ注解来指定目标对象(AspectJ中介绍)
基于BeanName生成代理
<!--根据BeanName自动生成代理对象-->
<!--通知-->
<bean id="befor" class="demo1.MyAdvice"/>
<bean id="after" class="demo1.MyAdvice2"/>
<!--目标-->
<bean id="userDao1" class="demo1.UserDaoImpl"/>
<bean id="userDao2" class="demo1.UserDaoImpl"/>
<!--自动代理生成器-->
<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
<property name="interceptorNames" value="after,befor"/>
<!--可使用 * 通配符-->
<property name="beanNames" value="userDao1,userDao2"/>
</bean>
我们会发现上面的配置中没有与切点相关的信息,的确这种方式定义的切面是普通切面,即所有目标的所有方法都会被增强
基于切点信息生成代理
<!-- 根据切面信息自动生成代理对象-->
<!--通知-->
<bean id="befor" class="com.kkb.demo1.MyAdvice"/>
<bean id="after" class="com.kkb.demo1.MyAdvice2"/>
<!--目标-->
<bean id="userDao1" class="demo1.UserDaoImpl"/>
<bean id="userDao2" class="demo1.UserDaoImpl"/>
<bean class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
<property name="advice" ref="after"/>
<property name="pattern" value=".*save"/>
</bean>
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/>
</beans>
DefaultAdvisorAutoProxyCreator
将会在容器中查找所有Advisor,然后按照re表达式来查找目标对象和切点,最后为目标对象生成代理对象;
两种方式存在一个共同点:都会将容器中的目标对象直接替换为代理对象,这样一来,我们在使用Bean时就不用在考虑获取的是原始对象还是代理对象了,直接使用即可
上述的配置单独拿出一种都是比较简单的,混在一起就很容易乱,你只需要记住,要使用AOP则必须明确的几个关键点及其关系:
-
目标
要被增强的Bean 没有什么特殊之处
-
通知
要增强的具体代码
-
切点
需要明确目标对象中要增强的方法是哪些,pointcut要做的事情
-
切面
需要明确在某个切点上应用某些通知,即advisor要做的事情
-
代理
需要明确目标
若是普通切面则 只需要明确,目标,和通知即可;
问题:
看起来指定切点的切面比普通切面更强大,那么为什么还需要普通切面呢?
那你设想一下,若你的需求是给所有方法全部加上日志输出,那这时采用普通切面是最简便的方式;