动态代理 与AOP (Aspect Oriented Program)面向方面编程,OOP为面向对象
"代理"的概念与作用 生活中的代理: 代理商,降低批发价格和运输成本 程序中的代理: 要为已存在的多个具有相同接口的目标类的各个方法增加一些系统功能, 例如,异常处理、日志、计算方法的运行时间、事务管理、等等 编写一个与目标类具有相同接口的代理类,代理类的每个方法调用目标类的相同方法, 并在调用方法时加上系统功能的代码。 (参看下页的原理图) 如果采用"工厂模式"和配置文件的方式进行管理,则不需要修改客户端程序, 在配置文件中配置是使用目标类、还是代理类,这样以后很容易切换, 譬如,想要日志功能时就配置代理类,否则配置目标类,这样,增加系统功能很容易, 以后运行一段时间后,又想去掉系统功能也很容易。 "AOP" Aspect oriented program 面向方面编程(J2EE) OOP Obect-o-p 面向对象 系统中存在交叉业务,一个交叉业务就是要切入到系统中的一个方面,如下所示: 安全 事务 日志 StudentService ------|----------|------------|------------- CourseService ------|----------|------------|------------- MiscService ------|----------|------------|------------- 用具体的程序代码描述交叉业务: method1 method2 method3 { { { ------------------------------------------------------切面 .... .... ...... ------------------------------------------------------切面 } } } 交叉业务的编程问题即为面向方面的编程(Aspect oriented program ,简称AOP),AOP的目标就是要使交叉业务模块化。可以采用将切面代码移动到原始方法的周围,这与直接在方法中编写切面代码的运行效果是一样的,如下所示: ------------------------------------------------------切面 func1 func2 func3 { { { .... .... ...... } } } ------------------------------------------------------切面 使用代理技术正好可以解决这种问题,代理是实现AOP功能的核心和关键技术。 要为系统中的各种接口的类增加代理功能,那将需要太多的代理类,全部采用静态代理方式, 将是一件非常麻烦的事情!写成百上千个代理类,是不是太累! JVM可以在运行期动态生成出类的字节码,这种动态生成的类往往被用作代理类,即"动态代理类"。 JVM生成的动态类必须实现一个或多个接口,所以,"JVM生成的动态类只能用作具有相同接口的目标类的代理。" CGLIB库可以动态生成一个类的子类,一个类的子类也可以用作该类的代理, 所以,如果要为一个没有实现接口的类生成动态代理类,那么可以使用CGLIB库。 代理类的各个方法中通常除了要调用目标的相应方法和对外返回目标返回的结果外, 还可以在代理方法中的如下四个位置加上系统功能代码: 1.在调用目标方法之前 2.在调用目标方法之后 3.在调用目标方法前后 4.在处理目标方法异常的catch块中 "Proxy" 代理超类的使用 创建实现了Collection接口的动态类和查看其名称,分析 Proxy.getProxyClass方法的各个参数。 编码列出动态类中的所有构造方法和参数签名 编码列出动态类中的所有方法和参数签名 创建动态类的实例对象 用反射获得构造方法 编写一个最简单的InvocationHandler类 调用构造方法创建动态类的实例对象,并将编写的InvocationHandler类的实例对象传进去 打印创建的对象和调用对象的没有返回值的方法和getClass方法,演示调用其他有返回值的方法报告了异常。 将创建动态类的实例对象的代理改成匿名内部类的形式编写 总结思考:让jvm创建动态类及其实例对象,需要给它提供哪些信息? 三个方面: 1.生成的类中有哪些方法,通过让其实现哪些接口的方式进行告知; 2.产生的类字节码必须有个一个关联的类加载器对象; 3.生成的类中的方法的代码是怎样的,也得由我们提供。把我们的代码写在一个约定好了接口对象的方法中, 把对象传给它,它调用我的方法,即相当于插入了我的代码。提供执行代码的对象就是那个InvocationHandler对象, 它是在创建动态类的实例对象的构造方法时传递进去的。在上面的InvocationHandler对象的invoke方法中加一点代码, 就可以看到这些代码被调用运行了。 Proxy.getProxyClass --> .getConstructor --> .newInstance 使用 Proxy.newInstance方法可以直接一步就创建出代理对象。 Collection proxy2 = (Collection) Proxy.newProxyInstance( Collection.class.getClassLoader(), //参数1,类加载器 new Class[] {Collection.class}, //参数2,Class数组 new InvocationHandler(){ //参数3,接口型内部类对象 ArrayList target = new ArrayList();//要代理的类target @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //三个参数分别代表:对象 方法 参数 long beginTime = System.currentTimeMillis();//---------------------- Object retVal = method.invoke(target, args);//代理方法的返回值Object long endTime = System.currentTimeMillis(); //----------------------- System.out.println(method.getName()+" :time: "+(endTime-beginTime));//添加功能 return retVal; //return method.invoke(proxy, args); 递归死循环 } }); 代理方法的执行步骤分析 //proxy2.add("java");使用代理添加元素 $Proxy.add(Object object) {//一、方法调用 return handler.invoke(Object proxy, Method method, Object[] args);//二、返回值 } invoke-->{ return method.invoke(target,args)} //三、返回值的返回值 所以说: 代理方法操作的结果是直接受handler.invoke(...)方法控制的 //不能被代理的方法 System.out.println(proxy2.getClass().getName());//返回值为$Proxys0,而不是(targrt) ArrayList /* getClass()方法返回的结果分析:(API) 调用调用代理对象的从Object类继承的hashCode, equals, 或toString这几个方法时, 代理对象将调用请求转发给InvocationHandler对象,对于其他方法,则不转发调用请求。 */
代理的结构
--------------------------------------------------------------
"动态代理类的设计原理与结构",框架结构
----------------------------
AOP核心--面向切面编程
需要解决的问题:
AOP核心--面向切面编程 "动态代理类的设计原理与结构" 框架结构 需要解决的问题: 如果目标类对象已经固化到代码中,没有办法由用户传入进行设置 如果交叉业务(系统功能)也同样被固化到代码中,无法由用户自由配置。 解决方式: 定义框架功能,将需要动态加载的对象以参数的形式传入 将目标类对象移动到匿名局部类newInvocationHandler(){}的外部进行配置 将系统功能 (交叉业务) 通过参数传入InvocationHandler的局部匿名类 【注意】局部类只能访问外部final变量,所以目标对象外置后要加 final 延长生命周期 //系统功能应该有四种执行位置,异常中,方法前,方法后,方法前后 Spring将系统功能看做是用户对代码的建议,所以接口命名成Advice Spring规定封装业务代码的接口的方法应该传入目标类对象、目标类对象调用的方法和目标类对象调用的方法对应的参数 "分析动态代理的工作原理" 怎样将目标类传进去? 直接在InvocationHandler实现类中创建目标类的实例对象,可以看运行效果和加入日志代码,但没有实际意义。 为InvocationHandler实现类注入目标类的实例对象,不能采用匿名内部类的形式了。 让匿名的InvocationHandler实现类访问外面方法中的目标类实例对象的final类型的引用变量。 将创建代理的过程改为一种更优雅的方式,eclipse重构出一个getProxy方法绑定接收目标同时返回代理对象, 让调用者更懒惰,更方便,调用者甚至不用接触任何代理的API。 将系统功能代码模块化,即将切面代码也改为通过参数形式提供,怎样把要执行的系统功能代码以参数形式提供? 把要执行的代码装到一个对象的某个方法里,然后把这个对象作为参数传递,接收者只要调用这个对象的方法, 即等于执行了外界提供的代码!为bind方法增加一个Advice参数。
AOP应用:实现一个类似Spring的可配置框架
操作步骤
Spring 框架核心技术 BeanFactory & AOP "实现AOP功能的封装与配置" 工厂类"BeanFactory"负责创建目标类或代理类的实例对象,并通过配置文件实现切换。其getBean方法 根据参数字符串返回一个相应的实例对象,如果参数字符串在配置文件中对应的类名不是ProxyFactoryBean, 则直接返回该类的实例对象,否则,返回该类实例对象的getProxy方法返回的对象。 BeanFactory的构造方法接收代表配置文件的输入流对象, 配置文件"config.properties)"格式如下: #xxx=java.util.ArrayList //普通的bean xxx=cn.itcast.ProxyFactoryBean//target 和 advice 都是为 ProxyFactoryBean服务的,通过配置来指定 xxx.target=java.util.ArrayList//后缀名代表对象类型,key = name+"target" xxx.advice=cn.itcast.MyAdvice//value 为MyAdvice类的路径 框架是固定的,配置文件可以进行修改,以适应不同的需求 配置文件的key 是框架中要用到的变量引用,value是要操作的类 "ProxyFacotryBean"代理类 充当封装生成动态代理的工厂,需要为工厂类提供哪些配置参数信息? 目标 target 通知 advice 编写客户端应用"AopFrameworkTest"类: 编写实现Advice接口的类和在配置文件中进行配置 调用BeanFactory获取对象 调用方法进行测试
代码实现:
编写BeanFactory
package aopframework; import java.io.IOException; import java.io.InputStream; import java.util.Properties; import enhance_2.Advice; public class BeanFactory { Properties props = new Properties(); public BeanFactory(InputStream ips) throws IOException { props.load(ips);//加载 } public Object getBean(String name) throws Exception{ Object bean = null; String className = props.getProperty(name); Class clazz = Class.forName(className); bean = clazz.newInstance();//JavaBean的特性:必须有一个不带参数的构造方法 if(bean instanceof ProxyFactoryBean){//如果是代理 (参见配置文件) Object proxy = null; ProxyFactoryBean proxyFactoryBean = (ProxyFactoryBean)bean;//转型 Advice advice = (Advice) Class.forName(props.getProperty(name + ".advice")).newInstance() ; Object target = Class.forName(props.getProperty(name + ".target")).newInstance() ; proxyFactoryBean.setAdvice(advice); proxyFactoryBean.setTarget(target); proxy = proxyFactoryBean.getProxy(); return proxy; //返回proxy } return bean; //返回bean }
编写ProxyFactoryBean代理类
package aopframework; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import enhance_2.Advice; public class ProxyFactoryBean { private Advice advice; private Object target; public Advice getAdvice() { return advice; } public void setAdvice(Advice advice) { this.advice = advice; } public Object getTarget() { return target; } public void setTarget(Object target) { this.target = target; } //advice 和target两个对象不再作为参数而是作为成员变量存在 public Object getProxy() { //使用Proxy.newProxyInstance 静态方法直接创建代理对象 Object proxy = Proxy.newProxyInstance( target.getClass().getClassLoader(), //参数1,类加载器 target.getClass().getInterfaces(), //参数2,Class数组 new InvocationHandler(){ //参数3,接口型内部类对象 @Override //三个参数分别代表:对象 方法 参数 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //---------------------- advice.beforeMethod(method); Object retVal = method.invoke(target, args);//代理方法的返回值Object advice.afterMethod(method); //----------------------- return retVal; } }); return proxy; } }
package aopframework; import java.io.IOException; import java.io.InputStream; import java.util.Collection; public class AopFrameworkTest { /** * @param args * @throws Exception * @throws IOException */ public static void main(String[] args) throws IOException, Exception { //通过配置文件来获取bean 和proxy InputStream ips = AopFrameworkTest.class.getResourceAsStream("config.properties"); Object bean = new BeanFactory(ips).getBean("xxx"); System.out.println(bean.getClass().getName()); ((Collection)bean).clear(); } }
【注意】:配置文件中的value必须是已经存在的的类
配置文件的key 是框架中的变量引用,可以通过修改对应的value来对指定的类进行操作
运行结果:可以发现该框架按照配置文件进行了指定的操作
【总结】
通过学习动态代理与AOP,能够了解框架的实现原理,学习模块化设计的的思想,对于日后主流开源框架的学习也是大有裨益的
再次深化 房子(框架) 、门(用户类)、锁(工具类)的概念
框架的本身建造起来并不容易,但是使用起来确实无比简单,只需修改配置文件即可达到目的,尤其是其开源性使初级的开发者也能够
站在巨人的肩膀上前行,避免了大量重复的操作