学习Spring必学的Java基础知识(2)----动态代理

 

参考:http://www.iteye.com/topic/1123293


是什么?

AOP称为面向切面编程,在程序开发中主要用来解决一些系统层面上的问题,比如日志,事务,权限等待;
 

一 AOP的基本概念

1、横切关注点

1.1 具有公共逻辑的,与其他模块的核心逻辑纠缠在一起的行为称为“横切关注点(Crosscutting Concern)”

1.2 对哪些方法进行拦截,拦截后怎么处理,这些关注点称之为横切关注点

2、切面(aspect)

2.1 类是对物体特征的抽象,切面就是对横切关注点的抽象

2.2 相同代码的抽象

3、连接点(joinpoint)

3.1 被拦截到的点,因为Spring只支持方法类型的连接点,所以在Spring中连接点指的就是被拦截到的方法,实际上连接点还可以是字段或者构造器

3.2 它是个虚的概念,可以理解成切入点的集合,它只是对应用程序所有需要进行插入切面的一个统称,但是记住 每一个切入点必须有对应的连接点,因为在运行期间, spring 就是根据这些切入点的定义 ,将通知或者拦截器插入到具体的连接点上,简言之,切入点是具体的,一个类,连接点是虚的,所以说,一切入点对多连接点

4、切入点(pointcut)

4.1 对连接点进行拦截的定义

4.2 需要对那些方法进行切入, 竟然是某种方法,那么肯定要对这些方法,进行一个查询 这里就用到了正则表达式,去查                  询。                 <aop:pointcut id="loggerCutpoint" expression="execution(*com.xxq.demo.spring.boot.zframe.demo.aspect.AspectDemoHelloInterface.*(..)) " />   包名 类名 下面的(..) 所有方法 

5、通知(advice)

所谓通知指的就是指拦截到连接点之后要执行的代码,通知分为前置、后置、异常、最终、环绕通知五类

6、目标对象

代理的目标对象

7、织入(weave)

将切面应用到目标对象并导致代理对象创建的过程

8、引入(introduction)

在不修改代码的前提下,引入可以在运行期为类动态地添加一些方法或字段


为什么?

一:

AOP(Aspect Oriented Programming),即面向切面编程,可以说是OOP(Object Oriented Programming,面向对象编程)的补充和完善。OOP引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。不过OOP允许开发者定义纵向的关系,但并不适合定义横向的关系,例如日志功能。日志代码往往横向地散布在所有对象层次中,而与它对应的对象的核心功能毫无关系对于其他类型的代码,如安全性、异常处理和透明的持续性也都是如此,这种散布在各处的无关的代码被称为横切(cross cutting),在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。

AOP技术恰恰相反,它利用一种称为"横切"的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为"Aspect",即切面。所谓"切面",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。

使用"横切"技术,AOP把软件系统分为两个部分:核心关注点横切关注点业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事物。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。

二:

AOP的核心思想 “将应用程序中的商业逻辑,同对其提供支持的通用服务进行分离”;

三:

3.1. 就是代码混乱,就是不是核心处理的代码也出现在核心的类里面,以后维护的时候,我还要去区分这个代码是什么来的,;

3.2. 就是代码太分散,就是当以后维护的时候需求发生变化的时候,比如我输出日志的格式发生改变,以前日志是输出XXXXXXXX,我现在要求日志输出时AAAAAAAAA那这样我是不是要每个类的去修改?比如我又几千几万行代码,那这样耗费的时间很多

这些代码出现在软件里面,因此,我们会产生一个疑问,是不是可以对这些相同的代码,进行一个抽象?

于是乎,就提出了面向切面编程。


怎么做?

之前的一个插入逻辑处理:

package com.xxq.rest.java.demo.aop;

public interface OperateService {
	void insert();

}


package com.xxq.rest.java.demo.aop;

public class OperateServiceImpl implements OperateService {

	@Override
	public void insert() {
		long startTiem = System.currentTimeMillis();
		System.out.println("我是日志,正在进行插入操作");
		System.out.println("插入操作");
		// 模拟数据插入
		try {
			Thread.sleep(2000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		Long endTime = System.currentTimeMillis() - startTiem;
		System.out.println("插入数据成功... 所耗费时间 : " + endTime + "ms");
	}

}



package com.xxq.rest.java.demo.aop;

public class TestMain {
	public static void main(String[] args) {
		OperateService operateService = new OperateServiceImpl();
		operateService.insert();
	}
}

 这个代码有一特性即 :应用程序中的商业逻辑,同对其提供支持的通用服务在一起了;

这样不好对其进行维护,以后我们要修改打印日志的格式,需要改动的地方非常大,这样编码的格式,违背了单一职责原则,以及开放封闭原则;

 对其打印日志的功能,可将其看成是一个横切关注点(Crosscutting Concern),然后具体是哪个service的方法,将其设为切入点(pointcut), 

xml实现,spring AOP 对其进行改造:


package com.xxq.rest.java.demo.aop;

/**
 * 打印日志的抽象类,横切切入点,抽象成类
 * @author Administrator
 *
 */
public class LogManager {
	
  public void outBefoerPutLog()
  {
	  System.out.println("我是日志,正在进行插入操作");
  }
  
  public void outAfeterPutLog()
  {
		System.out.println("插入数据成功...");
  }
  
}


package com.xxq.rest.java.demo.aop;

/**
 * 核心业务,插入接口
 * @author Administrator
 *
 */
public interface OperateService {
	void insert();

}


package com.xxq.rest.java.demo.aop;
/**
 * 核心业务的实现类
 * @author Administrator
 *
 */
public class OperateServiceImpl implements OperateService {

	@Override
	public void insert() {
		// 模拟数据插入
		try {
			Thread.sleep(2000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("插入数据...");
	} 

}



package com.xxq.rest.java.demo.aop;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class ApoConfigXmlMain {
	public static void main(String[] args) {
		ApplicationContext ctx = new ClassPathXmlApplicationContext("/com/xxq/rest/java/demo/aop/aop.xml");
		OperateService bean = (OperateService) ctx.getBean("operateServiceImpl");
		bean.insert();
	}
}



 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:tx="http://www.springframework.org/schema/tx"
	xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop-4.2.xsd">
	<!-- 日志 横切关注点 抽象成类 分散在项目各个位置的代码进行一个聚合,成为的一个类 -->
	<bean id="logManager" class="com.xxq.rest.java.demo.aop.LogManager"></bean>
	<!-- 核心业务类 -->
	<bean id="operateServiceImpl" class="com.xxq.rest.java.demo.aop.OperateServiceImpl"></bean>

   <!-- aop配置 -->
	<aop:config>
		<aop:pointcut id="addMethod" expression="execution(* com.xxq.rest.java.demo.aop.OperateService.*(..))" />
		<aop:aspect id="logAspect" ref="logManager">
			<aop:before method="outBefoerPutLog" pointcut-ref="addMethod" />
			<aop:after method="outAfeterPutLog" pointcut-ref="addMethod"/>
		</aop:aspect>

	</aop:config>

</beans>

 打印结果:

 

log4j:WARN No appenders could be found for logger (org.springframework.core.env.StandardEnvironment).
log4j:WARN Please initialize the log4j system properly.
log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.
我是日志,正在进行插入操作
插入数据...
插入数据成功...

 我们已经初步实现了,但是这个aop-config,到底做了什么操作了?是如何将主核心业务代码,跟对其支持公共的通用服务,进行一个整合,也就是织入(weave),这里我们就要提出另一个函数,jDK代理;chlib代理

即: 当某个方法需要进行性能监视,就必须调整方法代码,在方法体前后分别添加上开启性能监视和结束性能监视的代码。这些非业务逻辑的性能监视代码破坏了ForumServiceImpl业务逻辑的纯粹性。我们希望通过代理的方式,将业务类方法中开启和结束性能监视的这些横切代码从业务类中完全移除。并通过JDK动态代理技术或CGLib动态代理技术将横切代码动态织入到目标方法的相应位置。


 JDK动态代理 

JDK 1.3以后,Java提供了动态代理的技术,允许开发者在运行期创建接口的代理实例。在Sun刚推出动态代理时,还很难想象它有多大的实际用途,现在我们终于发现动态代理是实现AOP的绝好底层技术。 

JDK的动态代理主要涉及到java.lang.reflect包中的两个类:Proxy和InvocationHandler。其中InvocationHandler是一个接口,可以通过实现该接口定义横切逻辑,并通过反射机制调用目标类的代码,动态将横切逻辑和业务逻辑编织在一起。 

而Proxy利用InvocationHandler动态创建一个符合某一接口的实例,生成目标类的代理对象。这样讲一定很抽象,我们马上着手使用Proxy和InvocationHandler这两个魔法戒对上一节中的性能监视代码进行革新。 

package com.xxq.rest.spring.xml.jdk;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

/**
 * 
 * @author xiaoqiang
 *
 */
public class LogManagerHandler implements InvocationHandler {

	private Object target;

	public LogManagerHandler(Object target) { // ②target为目标的业务类
		this.target = target;
	}

	/**
	 * 1.invoke()方法中 LogManager.outBefoerPutLog(); LogManager.outAfeterPutLog();
	 * 粗体所示部分的代码为性能监视的横切代码,我们发现,横切代码只出现一次,而不是原来那样星洒各处。
	 * 
	 * 2,method.invoke()语句通过Java反射机制间接调用目标对象的方法,
	 * 这样InvocationHandler的invoke()方法就将横切逻辑代码和业务类方法的业务逻辑代码(③-2)编织到一起了,
	 * 所以我们可以将InvocationHandler看成是一个编织器。
	 */
	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // ③
		LogManager.outBefoerPutLog();// ③-1
		Object obj = method.invoke(target, args);// ③-2通过反射方法调用业务类的目标方法
		LogManager.outAfeterPutLog();// ③-1
		return obj;
	}

}

 

首先,我们实现InvocationHandler接口,该接口定义了一个 invoke(Object proxy, Method method, Object[] args)的方法,proxy是最终生成的代理实例,一般不会用到;method是被代理目标实例的某个具体方法,通过它可以发起目标实例方法的反射调用;args是通过被代理实例某一个方法的入参,在方法反射调用时使用。 
 

此外,我们在构造函数里通过target传入希望被代理的目标对象,如②处所示,在InvocationHandler接口方法invoke(Object proxy, Method method, Object[] args)里,将目标实例传给method.invoke()方法,调用目标实例的方法, 

创建代理实例方法:

package com.xxq.rest.spring.xml.jdk;

import java.lang.reflect.Proxy;

public class TestJdkMain {
	public static void main(String[] args) {
		// ①希望被代理的目标业务类
		OperateService target = new OperateServiceImpl();

		// ②将目标业务类和横切代码编织到一起
		LogManagerHandler handler = new LogManagerHandler(target);

		// ③根据编织了目标业务类逻辑和性能监视横切逻辑的InvocationHandler实例创建代理实例
		System.out.println(target.getClass().getClassLoader());
		OperateService newProxyInstance = (OperateService) Proxy.newProxyInstance(target.getClass().getClassLoader(),
				target.getClass().getInterfaces(), handler);

		// ④调用代理实例
		newProxyInstance.insert();
	}
}

上面的代码完成业务类代码和横切代码的编织工作并生成了代理实例。

 在②处,我们让LogManager将日志打印横切逻辑编织到OprateService实例中,

然后在③处,通过Proxy的newProxyInstance()静态方法为编织了业务类逻辑和日志打印逻辑的handler创建一个符合ForumService接口的代理实例。该方法的第一个入参为类加载器第二个入参为创建代理实例所需要实现的一组接口;第三个参数是整合了业务逻辑和横切逻辑的编织器对象。 

按照③处的设置方式,这个代理实例实现了目标业务类的所有接口,即OperateServiceimpl的OperateService接口。这样,我们就可以按照调用OperateService接口实例相同的方式调用代理实例,

如④所示。运行以上的代码,输出以下信息: 

 

sun.misc.Launcher$AppClassLoader@2a139a55
我是日志,正在进行插入操作
插入数据...
插入数据成功...

我们发现,程序的运行效果和直接在业务类中编写性能监视逻辑的效果一致,但是在这里,原来分散的横切逻辑代码已经被我们抽取到LogManagerhander中。当其他业务类(如UserService、SystemService等)的业务方法也需要使用性能监视时,我们只要按照代码清单6-7相似的方式,分别为它们创建代理对象就可以了。下面,我们通过时序图描述通过创建代理对象进行业务方法调用的整体逻辑,以进一步认识代理对象的本质,如图6-3所示。 

 

我们在上图中使用虚线的方式对通过Proxy创建的ForumService代理实例加以凸显,OperateService代理实例内部利用LogManagerHander整合横切逻辑和业务逻辑。调用者调用代理对象的rinsert()方法时,上图的内部调用时序清晰地告诉我们实际上所发生的一切。 


CGLib动态代理 

为什么会出现?

1.使用JDK创建代理有一个限制,即它只能为接口创建代理实例,这一点我们可从Proxy的接口newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h)的方法签名中就看得很清楚:第二个入参interfaces就是需要代理实例实现的接口列表。虽然面向接口编程的思想被很多大师级人物(包括Rod Johnson)推崇,但在实际开发中,许多开发者也对此深感困惑:难道对一个简单业务表的操作也需要老老实实地创建5个类(领域对象类、Dao接口,Dao实现类,Service接口和Service实现类)吗?难道不能直接通过实现类构建程序吗?对于这个问题,我们很难给出一个孰好孰劣的准确判断,但我们确实发现有很多不使用接口的项目也取得了非常好的效果(包括大家所熟悉的SpringSide开源项目)。 

2.对于没有通过接口定义业务方法的类,如何动态创建代理实例呢?JDK的代理技术显然已经黔驴技穷,CGLib作为一个替代者,填补了这个空缺。 
 

3.CGLib采用非常底层的字节码技术,可以为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,并顺势织入横切逻辑。

这么实现?

下面,我们采用CGLib技术,编写一个可以为任何类创建织入性能监视横切逻辑代理对象的代理创建器。

 

package com.xxq.rest.spring.xml.jdk;

import java.lang.reflect.Method;

import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

public class CglibProxy implements MethodInterceptor {

	private Enhancer enhancer = new Enhancer();

	public Object getProxy(Class clazz) {
		enhancer.setSuperclass(clazz); // ① 设置需要创建子类的类
		enhancer.setCallback(this);
		return enhancer.create(); // ②通过字节码技术动态创建子类实例
	}

	// ③拦截父类所有方法的调用
	@Override
	public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
		LogManager.outBefoerPutLog();// ③-1
		Object invoke = proxy.invoke(obj, args);// ③-2
		LogManager.outAfeterPutLog();// ③-1通过代理类调用父类中的方法
		return invoke;
	}

}

在上面代码中,用户可以通过getProxy(Class clazz)为一个类创建动态代理对象,该代理对象通过扩展clazz创建代理对象。在这个代理对象中,我们织入日志监视的横切逻辑(③-1)。intercept(Object obj, Method method, Object[] args,MethodProxy proxy)是CGLib定义的Interceptor接口的方法,它拦截所有目标类方法的调用,obj表示目标类的实例method为目标类方法的反射对象;args为方法的动态入参而proxy为代理类实例。 


代理知识小结 

1. Spring AOP的底层就是通过使用JDK动态代理或CGLib动态代理技术为目标Bean织入横切逻辑。在这里,我们对前面两节动态创建代理对象作一个小结。 

我们虽然通过LogaManagerHandler或CglibProxy实现了性能监视横切逻辑的动态织入,但这种实现方式存在三个明显需要改进的地方: 

1)目标类的所有方法都添加了性能监视横切逻辑,而有时,这并不是我们所期望的,我们可能只希望对业务类中的某些特定方法添加横切逻辑; 
2)我们通过硬编码的方式指定了织入横切逻辑的织入点,即在目标类业务方法的开始和结束前织入代码; 
3)我们手工编写代理实例的创建过程,为不同类创建代理时,需要分别编写相应的创建代码,无法做到通用。 

以上三个问题,在AOP中占用重要的地位,因为Spring AOP的主要工作就是围绕以上三点展开:Spring AOP通过Pointcut(切点)指定在哪些类的哪些方法上织入横切逻辑,通过Advice(增强)描述横切逻辑和方法的具体织入点(方法前、方法后、方法的两端等)。此外,Spring通过Advisor(切面)将Pointcut和Advice两者组装起来。有了Advisor的信息,Spring就可以利用JDK或CGLib的动态代理技术采用统一的方式为目标Bean创建织入切面的代理对象了。 

JDK动态代理所创建的代理对象,在JDK 1.3下,性能强差人意。虽然在高版本的JDK中,动态代理对象的性能得到了很大的提高,但是有研究表明,CGLib所创建的动态代理对象的性能依旧比JDK的所创建的代理对象的性能高不少(大概10倍)。但CGLib在创建代理对象时所花费的时间却比JDK动态代理多(大概8倍),所以对于singleton的代理对象或者具有实例池的代理,因为无须频繁创建代理对象,所以比较适合用CGLib动态代理技术,反之适合用JDK动态代理技术。值得一提的是,由于CGLib采用动态创建子类的方式生成代理对象,所以不能对目标类中的final方法进行代理。 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值