回顾:
在上一篇文章“演进式例解AOP:Java 动态代理”中用一个打印报表的例子很简单地温习了一下 Java 中的动态代理实现,其实最终目的如标题,即利用动态代理结合之前写的关于控制反转(IoC)容器、依赖注入(DI)、读外部配置文件,来集中式地、简单地模拟一下Spring中所具有的IoC、DI、AOP功能。
前面相关的文章有:其一:引入容器,Service Locator、其二:引入IoC,DI 、其三:结合配置文件完善DI。以下仍旧是用“演进式例解AOP:Java 动态代理”中的报表生成的需求来进行模拟实现。
问题描述:
与前面相同,即:开发一个能够根据本地文件路径、远程文件路径生成HTML、PDF格式的报表的应用。由于不同操作系统下的文件路径有不同的路径分隔符,因此这里存在一个特殊要求:接收到文件路径生成报表之前必须验证该文件路径的合法性。这里假设该应用系统可以根据需要决定是否在报表生成【之前、之后】进行日志记录(这里强调了before、after 是为了配合AOP的前置通知、后置通知进行模拟,而验证功能一般只是前置)。
注意这里的特殊要求是:要通过外部配置文件更加灵活地决定日志记录的使用与否,这种额外添加功能的特点是AOP横切关注点灵活的体现。而前面的动态代理只是硬编码到具体实现当中去了。
解决方案:
既然已经讲明需要读取外部的配置文件了,那么就和之前的“结合配置文件完善DI”类似,通过一个 BeanUtil 工具类来负责根据配置文件中的组件声明、组件间关系来分别创建对象或执行依赖注入动作。
问题在于:日志记录这种额外功能(Crosscutting Concern,即横切关注点)如何可选择性地添加到具体的应用系统当中去。
注:关于横切关注点的我也没有深入探究(毕竟能力、经验非常有限),只了解有这么一回事及其简单应用场景而已(两次读过《冒号课堂》一书又给忘了…但极力推荐本书),暂时先理解为需要额外添加的功能即可(例如日志记录),以下可能会时不时用到这概念。
实现方法:
为了使得这种before(前置)、after(后置)添加的额外功能更加通用,这里利用Java中的OO概念来抽象出Advice(通知)这一源自AOP的重要概念。关于Advice的调用执行(即横切关注点的执行),是在 ProxyHandler 类中的invoke() 方法利用了Java的动态代理技术来实现的:当beforeAdvice或afterAdvice被注入之后,则相对应地调用;若未注入Advice,则效果如普通方法调用一样。
另外,这里的外部配置文件依然采用 .properties 格式,即 Key-Value 形式,包含组件声明、组件间依赖注入关系,具体如下(注意其中组件名与具体实现代码相关):
- #definebeans
- Bean.target=AOP.aop_di.RemoteReportCreator
- Bean.targetProxy=AOP.aop_di.ProxyHandler
- Bean.logBeforeAdvice=AOP.aop_di.LogBeforeAdvice
- Bean.logAfterAdvice=AOP.aop_di.LogAfterAdvice
- #defineDI
- DI.targetProxy.target=target
- DI.targetProxy.beforeAdvice=logBeforeAdvice
- DI.targetProxy.afterAdvice=logAfterAdvice
好像下面的类图结构、具体代码实现然而让人更加容易明白,这里就不再说得更复杂难理解了。注意其中绿色背景的“AOP_DI 核心结构”是指类比Spring 框架,其他的则为框架使用者自定义(例如ReportCreator 类结构)或必须实现的子类(例如LogBeforeAdvice 子类)。对了,我还没画时序图呢,有时序图肯定会更加容易直观地理解。
依据具体需求和分析,设计类图框架如下:
完整的简单代码实现如下:
- /**
- *报表生成的公共接口
- */
- interfaceReportCreator{
- publicvoidgetHtmlReport(Stringpath);
- publicvoidgetPdfReport(Stringpath);
- }
- /**
- *用于根据【本地】文件路径生成报表
- */
- classLocalReportCreatorimplementsReportCreator{
- publicvoidgetHtmlReport(Stringpath){
- System.out.println("根据【本地】文件生成【HTML】格式的报表...");
- }
- publicvoidgetPdfReport(Stringpath){
- System.out.println("根据【本地】文件生成【PDF】格式的报表...");
- }
- }
- /**
- *用于根据【远程】文件路径生成报表
- */
- classRemoteReportCreatorimplementsReportCreator{
- publicvoidgetHtmlReport(Stringpath){
- System.out.println("根据【远程】文件生成【HTML】格式的报表...");
- }
- publicvoidgetPdfReport(Stringpath){
- System.out.println("根据【远程】文件生成【PDF】格式的报表...");
- }
- }
- /**
- *代理提供者:实现invoke方法,构造具有横切关注点的动态代理对象
- */
- classProxyHandlerimplementsInvocationHandler{
- privateObjecttarget;//被代理的目标对象
- privateAdvicebeforeAdvice;//before横切关注点
- privateAdviceafterAdvice;//after横切关注点
- publicProxyHandler(){
- //空构造方法
- }
- /**
- *3个setter方式的用于依赖注入
- */
- publicvoidsetTarget(Objecttarget){
- this.target=target;
- }
- publicvoidsetBeforeAdvice(AdvicebeforeAdvice){
- this.beforeAdvice=beforeAdvice;
- }
- publicvoidsetAfterAdvice(AdviceafterAdvice){
- this.afterAdvice=afterAdvice;
- }
- /**
- *实现invoke()方法,添加before、after关注点以实现AOP
- */
- publicObjectinvoke(Objectproxy,Methodmethod,Object[]args)
- throwsThrowable{
- //在被代理对象业务方法前后添加横切关注点方法
- this.aspect(this.beforeAdvice,"before");
- Objectresult=method.invoke(this.target,args);
- this.aspect(this.afterAdvice,"after");
- returnresult;
- }
- /**
- *依据是否注入横切关注点来决定before、after的调用
- */
- privatevoidaspect(Adviceadvice,StringaspectName)throwsException{
- if(advice!=null){
- Classc=advice.getClass();
- Method[]methods=c.getMethods();
- for(Methodm:methods){
- if(aspectName.equals(m.getName())){
- //以null参数调用已实现的before、after方法
- methods[0].invoke(advice,null);
- }
- }
- }
- }
- /**
- *静态工厂方法,用于获取已注入before、after的动态代理实例
- */
- publicObjectgetProxy(Objecttarget){
- ClasstargetClass=target.getClass();
- /*
- *loader:目标类的类加载器interfaces:目标类已实现的接口handler:
- *转发方法调用的调用处理类实例,这里是当前Handler
- */
- ClassLoaderloader=targetClass.getClassLoader();
- Class[]interfaces=targetClass.getInterfaces();
- //创建并返回动态代理类实例
- returnProxy.newProxyInstance(loader,interfaces,this);
- }
- }
- /**
- *工厂类:从外部读取配置信息创建并注入所需对象
- */
- classProxyFactory{
- //代理对象的提供者
- privateProxyHandlerhandler;
- //被代理对象
- privateObjecttarget;
- //所有组件类的集合
- privateMap<String,Object>beans;
- /**
- *读取外部配置文件初始化所有组件间的关系
- */
- publicProxyFactory(StringconfigFile){
- beans=newHashMap<String,Object>();
- try{
- Propertiesprops=newProperties();
- props.load(newFileInputStream(configFile));
- //bean声明及依赖注入的key-value对
- Map<String,String>beanKV=newHashMap<String,String>();
- Map<String,String>diKV=newHashMap<String,String>();
- for(Map.Entryentry:props.entrySet()){
- Stringkey=(String)entry.getKey();
- Stringvalue=(String)entry.getValue();
- if(key.startsWith("Bean")){
- beanKV.put(key,value);
- }elseif(key.startsWith("DI")){
- diKV.put(key,value);
- }
- }
- //一定要先处理bean声明再进行依赖注入
- this.processKeyValue(beanKV);
- this.processKeyValue(diKV);
- }catch(Exceptione){
- e.printStackTrace();
- }
- }
- /**
- *处理key-value
- */
- privatevoidprocessKeyValue(Map<String,String>map)throwsException{
- for(Map.Entryentry:map.entrySet()){
- Stringkey=(String)entry.getKey();
- Stringvalue=(String)entry.getValue();
- this.handleEntry(key,value);
- }
- }
- /**
- *利用工具类BeanUtil处理key-value对,即创建或注入bean
- */
- privatevoidhandleEntry(Stringkey,Stringvalue)throwsException{
- String[]keyParts=key.split("\\.");
- Stringtag=keyParts[0];
- if("Bean".equals(tag)){
- //组件定义:利用反射实例化该组件
- Objectbean=Class.forName(value).newInstance();
- System.out.println("组件定义:"+bean.getClass().getName());
- beans.put(keyParts[1],bean);
- }elseif("DI".equals(tag)){
- //依赖注入:获取需要bean的主体,以及被注入的实例
- Objectbean=beans.get(keyParts[1]);
- ObjectfieldRef=beans.get(value);
- System.out.println("依赖注入:"+bean.getClass().getName()+
- "."+fieldRef.getClass().getName());
- BeanUtil.setProperty(bean,keyParts[2],fieldRef);
- }
- }
- /**
- *针对Factory已创建的Target获取代理对象
- */
- publicObjectgetProxy(StringproxyName,StringtargetNanme){
- Objecttarget=this.beans.get(targetNanme);
- if(target!=null){
- this.handler=(ProxyHandler)this.beans.get(proxyName);
- returnthis.handler.getProxy(target);
- }
- returnnull;
- }
- }
- /**
- *工具类:处理配置文件中K-V对,即创建或注入bean
- */
- publicclassBeanUtil{
- /**
- *利用反射进行依赖注入
- *@parambean需要注入外部依赖的主体类实例
- *@paramfieldName需要注入的字段名
- *@paramfieldRef被注入的组件实例
- *@throwsException
- */
- publicstaticvoidsetProperty(Objectbean,StringfieldName,
- ObjectfieldRef)throwsException{
- //获取主体类的完整名称
- StringclassName=getClassName(bean);
- //获取主体类的所有Method
- ClassbeanClass=Class.forName(className);
- Method[]methods=beanClass.getMethods();
- //准备对应setter()方法的完整名称
- StringsetterName="set"+fieldName.substring(0,1).toUpperCase()
- +fieldName.substring(1,fieldName.length());
- //遍历找到对应setter方法,并调用invoke()方法进行注入
- for(Methodm:methods){
- if(m.getName().equals(setterName)){
- m.invoke(bean,fieldRef);
- System.out.println("已调用"+m.getName()+"()向"+className
- +"注入"+getClassName(fieldRef));
- return;
- }
- }
- System.out.println(">>注入失败:"+className+"类中不存在"+fieldName
- +"字段对应的setter()方法...");
- }
- /**
- *根据Object实例获取类的完整名称
- */
- privatestaticStringgetClassName(Objecto){
- if(o==null){
- System.out.println("传入的Object实例为null...");
- returnnull;
- }
- StringfullName=o.toString();
- StringclassName=fullName.substring(0,fullName.indexOf("@"));
- returnclassName;
- }
- }
- //测试
- publicclassAOP_DI{
- publicstaticvoidmain(String[]args){
- //初始化环境配置
- ProxyFactoryproxyFactory=newProxyFactory("config.properties");
- //获取被代理后的Target对象
- ReportCreatorreportCreator=(ReportCreator)proxyFactory
- .getProxy("targetProxy","target");
- //使用被代理后的target对象提供的服务
- reportCreator.getHtmlReport("http://code.google.com/file/...");
- }
- }
根据最前面的配置文件信息,运行可得以下结果:
- 组件定义:AOP.aop_di.ProxyHandler
- 组件定义:AOP.aop_di.LogBeforeAdvice
- 组件定义:AOP.aop_di.LogAfterAdvice
- 组件定义:AOP.aop_di.RemoteReportCreator
- 依赖注入:AOP.aop_di.ProxyHandler.AOP.aop_di.LogBeforeAdvice
- 已调用setBeforeAdvice()向AOP.aop_di.ProxyHandler注入AOP.aop_di.LogBeforeAdvice
- 依赖注入:AOP.aop_di.ProxyHandler.AOP.aop_di.LogAfterAdvice
- 已调用setAfterAdvice()向AOP.aop_di.ProxyHandler注入AOP.aop_di.LogAfterAdvice
- 依赖注入:AOP.aop_di.ProxyHandler.AOP.aop_di.RemoteReportCreator
- 已调用setTarget()向AOP.aop_di.ProxyHandler注入AOP.aop_di.RemoteReportCreator
- 原业务方法被调用【之前】先打印日志...
- 根据【远程】文件生成【HTML】格式的报表...
- 原业务方法被调用【之后】再打印日志...
修改配置文件,即去掉其中的LogAfterAdvice的注入,如下:
- #definebeans
- Bean.target=AOP.aop_di.RemoteReportCreator
- Bean.targetProxy=AOP.aop_di.ProxyHandler
- Bean.logBeforeAdvice=AOP.aop_di.LogBeforeAdvice
- #defineDI
- DI.targetProxy.target=target
- DI.targetProxy.beforeAdvice=logBeforeAdvice
运行结果如下:
- 组件定义:AOP.aop_di.ProxyHandler
- 组件定义:AOP.aop_di.LogBeforeAdvice
- 组件定义:AOP.aop_di.RemoteReportCreator
- 依赖注入:AOP.aop_di.ProxyHandler.AOP.aop_di.LogBeforeAdvice
- 已调用setBeforeAdvice()向AOP.aop_di.ProxyHandler注入AOP.aop_di.LogBeforeAdvice
- 依赖注入:AOP.aop_di.ProxyHandler.AOP.aop_di.RemoteReportCreator
- 已调用setTarget()向AOP.aop_di.ProxyHandler注入AOP.aop_di.RemoteReportCreator
- 原业务方法被调用【之前】先打印日志...
- 根据【远程】文件生成【HTML】格式的报表...
小结:
似乎勉强达到了前面我所说的目标:利用动态代理结合之前写的关于控制反转(IoC)容器、依赖注入(DI)、读外部配置文件,来集中式地、简单地模拟一下Spring中所具有的IoC、DI、AOP功能。
@H_620_2502@其实问题存在不少,而且在我实现代码时也遇到一些问题,但现在篇幅已经挺长的了(不知道有没谁会坚持看完?哈…),可能会另外写一篇总结来记录一些我记忆较深刻的方面,我觉得其中更重要的是:写这几篇文章的意图是啥呢?从哪里得到思路的?:-D
瞧瞧以下您感兴趣的、相关的内容 ^_^
★结合配置文件、反射完善控制反转(IoC)、依赖注入(DI)