Spring Ioc管理AspectJ切面与运行时织入(LWT)应用方式实现容器内外切面一体化
Spring的AOP功能对于大部分的应用来说可以说是足够的,但这种AOP还是有不少情况下满足不了需求,而且Spring的AOP是通过其自身的代理实现的,如果因为某些原因不能或不想使用代理,例如ORM情况下的实体,一般由JPA、Hibernate,topLink等持久化框架创建的,并不在Spring容器的管理下,那就只好另想办法了。
AspectJ可以说是目前开源面向切面编程最强的实现,几乎能满足所有切面编程的需求,能管理任何你想管理的bean,并且能和Spring完美的整合在一起,那么有了能大小通吃的AspectJ,你还想用Spring自己的阉割过的AOP鸟炮么。。
管理容器内的切面大家一般都会用了,不会用的就看Spring文档的AOP部分。但管理容器外的切面,那就非得AspectJ亲自出面了,再进一步,如果能让 AspectJ 的容器外 切面也能享受 Spring Ioc 的待遇,那么Spring容器内外就能真正打成一片了,如此就能发挥无穷威力,做更多以前难以做到的事情。比如Entity的字段级授权控制。那就是小菜一碟了。
说了那么多虚的,下面给例子大家看看,就知道威力何在了(本例子需要给eclipse安装AJDT插件用来编译 切面类,也可以通过 iajc的 ant任务编译AspectJ切面类):
1.定义一些常用命名切点
package com.yotexs.aop; import org.aspectj.lang.annotation.Pointcut; /** * 命名切点类,定义一些基本的切点匹配模式,可以在任何其他的AspectJ切面定义中引用 * <p> * 用法:如匹配classpath中注解了@Repository的类的所有public的方法 * <pre> * @Around("com.yotexs.aop.Pointcuts.publicMethodOnly() && com.yotexs.aop.Pointcuts.atRepository()") * </pre> * @author bencmai 麦田枭鹰 QQ:36767935 */ public abstract class Pointcuts { /** 匹配public方法 */ @Pointcut("execution(public * *.*(..))") public void publicMethodOnly() {} /** 匹配public get方法 */ @Pointcut("execution(public * *.get*(..))") public void publicGetMethodOnly() {} /** 匹配@Repository注解过的类 */ @Pointcut("@within(org.springframework.stereotype.Repository)") public void atRepository() {} /** 匹配@Service注解过的类 */ @Pointcut("@within(org.springframework.stereotype.Service)") public void atService() {} /** 匹配@Configurable注解过的类 */ @Pointcut("@within(org.springframework.beans.factory.annotation.Configurable)") public void atConfigurable() {} }
2.以注解方式定义一个AspectJ(如果你喜欢用AspectJ语言,也可以用AspectJ语言编码方式定义切面)
package com.yotexs.aop.spring; import com.yotexs.ext.springsecurity.aspectj.AspectJCallback; import com.yotexs.ext.springsecurity.aspectj.AspectJSecurityInterceptor; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.Ordered; /** * Spring AspectJ 方式服切面配置 * * @author bencmai 麦田枭鹰 QQ:36767935 */ //@Component @Aspect public class SecurityAspectJ implements Ordered { protected final Logger logger = LoggerFactory.getLogger(getClass()); private int order = 1; //用于指定切面类之间的增强织入顺序 //注入Spring Security AspectJ方法安全拦截器处理器,此拦截器来自Spring容器内, //但可以同时用于容器内外的所有需要方法调用的安全监控和事后返回数据过滤的审查(不知道什么原因,网上还没有发现有文章介绍Spring Security 这种比MethodSecurityInterceptor强悍的多的用法) @Autowired private AspectJSecurityInterceptor securityInterceptor; /** 服务层命名切点--匹配注解了@Service的服务层暴露的public方法 */ @Pointcut("com.yotexs.aop.Pointcuts.publicMethodOnly()&& com.yotexs.aop.Pointcuts.atService()") public void atServiceLayer() {} /** 持久层命名切点--匹配注解了@Repository的持久层暴露的public方法 */ @Pointcut("com.yotexs.aop.Pointcuts.publicMethodOnly()&& com.yotexs.aop.Pointcuts.atRepository()") public void atDaoLayer() {} /** 领域层命名切点--匹配注解了@Configurable的持久层暴露的public get方法 */ @Pointcut("com.yotexs.aop.Pointcuts.publicGetMethodOnly()&& com.yotexs.aop.Pointcuts.atConfigurable()") public void atEntityLayer() {} /** 环绕增强容器内服务层--做服务调用安全和事后数据过滤检查。用于保护方法方法级 */ @Around("atServiceLayer()") public Object atServiceLayerDoMethodSecurityCheck(final ProceedingJoinPoint joinPoint) throws Throwable { final AspectJCallback callback = new AspectJCallback() {//回调封装 public Object proceedWithObject() throws Throwable { Object obj = null; try { obj = joinPoint.proceed(); } catch (Throwable e) { System.out.println("----Service层发生非事务性异常:"); logger.info("----Service层发生异常:", e); } return obj; } }; return this.securityInterceptor.invoke(joinPoint, callback); } /** 环绕增强容器内持久层 */ @Around("atDaoLayer()") public Object atDaoLayerDoMethodSecurityCheck(final ProceedingJoinPoint joinPoint) throws Throwable { logger.info("----执行:" + joinPoint.getSignature().toLongString()); Object obj = null; try { obj = joinPoint.proceed(); } catch (Throwable e) { System.out.println("----DAO层发生非事务性异常:"); logger.info("----DAO层发生异常:", e); } logger.info("----完成:" + joinPoint.getSignature().toLongString()); return obj; } /** 环绕增强容器外领域层对象--做字段访问权限检查 */ @Around("atEntityLayer()") public Object atEntityLayerDoFieldMethodSecurityCheck(final ProceedingJoinPoint joinPoint) throws Throwable { System.out.println("+++访问字段:" + joinPoint.getSignature().getName() + "方法"); final AspectJCallback callback = new AspectJCallback() {//回调封装 public Object proceedWithObject() throws Throwable { Object obj = null; try { obj = joinPoint.proceed(); } catch (Throwable e) { logger.info("----Entity层发生异常:", e); } return obj; } }; System.out.println("+++完成字段:" + joinPoint.getSignature().getName()); return this.securityInterceptor.invoke(joinPoint, callback); } @Override public int getOrder() { return order; } /** * 同一切面类中,依照增强在切面类中的声明顺序织入;<br> * 不同切面类中,各切面类实现{@link Ordered} 接口,则顺序号小的先织入;<br> * 不同切面类中,各切面类未实现{@link Ordered} 接口,织入顺序不确定; * * @param order */ public void setOrder(int order) { this.order = order; } }
3.在spring Xml schema中注册切面
<bean id="securityAspectJ" class="com.yotexs.aop.spring.SecurityAspectJ" factory-method="aspectOf"/>
注意:factory-method="aspectOf",因为我们的securityAspectJ 切面被当成spring容器中的一个bean注册到容器中,但这个bean并不是由spring去创建的,而是由 AspectJ自己创建的单例,细心的你可能觉得这里有古怪,
可能会问,看了上面第2点的SecurityAspectJ的代码,里面并没有 aspectOf 这个静态工厂方法,这样注册到spring容器里不会报错吗。那么让我们看看返编译后的SecurityAspectJ 代码先:
反编译后的SecurityAspectJ切面类
package com.yotexs.aop.spring; import ..; public class SecurityAspectJ implements Ordered { protected final Logger logger = LoggerFactory.getLogger(getClass()); private int order; private AspectJSecurityInterceptor securityInterceptor; private static Throwable ajc$initFailureCause; /* AspectJ 编译后给切面类织入的代码*/ public static final SecurityAspectJ ajc$perSingletonInstance; /* AspectJ 编译后给切面类织入的代码*/ ....... public Object atServiceLayerDoMethodSecurityCheck(final ProceedingJoinPoint joinPoint) throws Throwable { ....... } public Object atDaoLayerDoMethodSecurityCheck(ProceedingJoinPoint joinPoint) throws Throwable { ....... } public Object atEntityLayerDoFieldMethodSecurityCheck(final ProceedingJoinPoint joinPoint) throws Throwable { ....... } ..... /* AspectJ 编译后给切面类织入的代码*/ public static SecurityAspectJ aspectOf() { if (ajc$perSingletonInstance == null) throw new NoAspectBoundException("com.yotexs.aop.spring.SecurityAspectJ", ajc$initFailureCause); else return ajc$perSingletonInstance; } public static boolean hasAspect() { return ajc$perSingletonInstance != null; } private static void ajc$postClinit() { ajc$perSingletonInstance = new SecurityAspectJ(); } public static Logger ajc$inlineAccessFieldGet$com_yotexs_aop_spring_SecurityAspectJ$com_yotexs_aop_spring_SecurityAspectJ$logger(SecurityAspectJ securityaspectj) { return securityaspectj.logger; } static { try { ajc$postClinit(); } catch (Throwable throwable) { ajc$initFailureCause = throwable; } } }
所以,说SecurityAspectJ 是AspectJ自身通过静态工厂自己创建的,并不是由Spring去去创建的,而只是把它添加到容器中而已。
如果很不幸你在spring中注册AspectJ切面类时报错。请检查你有没有通过AJDT或者iajc的 ant任务编译AspectJ切面类,没编译的切面类里是不会存在上面反编译出来的代码的,就会报错。
4. 运行时织入(LTW) .
以上是静态织入(CTW)的做法。就是切面类了的增强方法中的代码在编译期间就已经分别织入到切点匹配上的目标类的链接点上了(织入到目标*.class的字节码中)。这种做法是性能比较好。但也存在局限,如你没有织入目标的源代码。或不想修改织入目标的字节码(可能这些*.class会用在不同的项目)时。就可以考虑使用运行时织入(LTW)。另外还有字节码织入(BTW).这里我们讨论LWT的方式
AspectJ默认情况下只支持代理方式的运行时织入,所以启动原生AspectJ的 LWT 程序,须指定 -javaagent:pathto/aspectjweaver.jar 的JVM参数,(CTW不用)。这就为同一个JVM多个Web应用带来了一点局限。尤其是托管环境下不好给JVM指定此参数(主机是你自己能完全控制的当然就没问题)。
在Spring容器整合环境下的无需指定-javaagent的JVM参数的 解决办法是在spring Xml schema添加:
4.A
<context:load-time-weaver />
此外如果是tomcat,改为添加,并把spring-tomcat-weaver.jar拷贝到tomcat安装目录的下的lib目录
<context:load-time-weaver weaver-class="org.springframework.instrument.classloading.ReflectiveLoadTimeWeaver" />
同时在发布应用的根目录或war中添加此配置文件([$approot]/META-INF/context.xml)
<!-- Tomcat context descriptor used for specifying a custom ClassLoader --> <Context path="/yotexs" reloadable="false"> <!-- please note that useSystemClassLoaderAsParent is available since Tomcat 5.5.20 / remove if previous versions are being used --> <Loader loaderClass="org.springframework.instrument.classloading.tomcat.TomcatInstrumentableClassLoader" useSystemClassLoaderAsParent="false"/> </Context>
4.B
在classpath下增加 [$classpath]/META-INF/aop.xml
<!DOCTYPE aspectj PUBLIC "-//AspectJ//DTD//EN" "http://www.eclipse.org/aspectj/dtd/aspectj.dtd"> <aspectj> <weaver options="-showWeaveInfo -XmessageHandlerClass:org.springframework.aop.aspectj.AspectJWeaverMessageHandler"> <include within="com.yotexs..*" /> </weaver> <aspects> <aspect name="com.yotexs.aop.spring.SecurityAspectJ" /> <!--定义抽象切面 <concrete-aspect name="com.yotexs.aspectj.EntityFieldSecurityAspectJ" extends="com.yotexs.aspectj.AbstractEntityFieldSecurityAspectJ"> <pointcut name="atServiceLayerScope" expression="execution(public * *.get*(..)) && @within(org.springframework.beans.factory.annotation.Configurable)" /> </concrete-aspect>--> </aspects> </aspectj>
总结。
经过以上配置。你就可以在应用的任何“层”上应用 AspectJ 切面了。使用AspectJ的意义在于突破Spring AOP 自身的局限。让你有完全free的感觉。又能享受Spring 的Ioc待遇。如果你还想借助Spring Security实现字段级授权。有了AspestJ管理容器外bean,说白了也只是把容器内的AbstractSecurityInterceptor 的方法从本来应用到容器内的服务层服务层方法拦截,给他通过AspectJ切面拖到容器外切入领域层而已。道理同样是应用方法拦截。这样通过Spring Security 中的ACL 负责定义记录级的授权。RBAC中的资源(Rourse)负责定义定义URL、ServiceMethod,FieldMethod的访问授权。
AspectJ可以做的东西非常多。以上只是简单的例子。enjoy it !!!