@zero1036
2018-04-26T10:53:22.000000Z
字数 8481
阅读 4169
Java-Spring
参考自:AOP面向方面编程
Spring AOP本质就是配合使用JDK Proxy动态代理和CGLIB工具,从而实现方法的切入。Spring会优先使用JDK动态代理,当调用方法不是接口方法时,选择使用CGLIB,参考大牛的总结:Spring AOP源码分析(二)JDK动态代理和CGLIB介绍,如下:
原理分析见下:
public class ProxyTest {
public static void main(String[] args) {
Say say1 = new Person("lg");
say1 = (Say) JDKDynamicProxy.createProxy(say1);
say1.sayHello();
System.out.println("-------------------------------");
Say say2 = new Animals();
say2 = (Say) JDKDynamicProxy.createProxy(say2);
say2.sayHello();
}
}
public class JDKDynamicProxy {
public static Object createProxy(final Object target) {
/**
* newProxyInstance参数解释:
* classLoader:将目标类接口的实现类加载到jvm,需要用到ClassLoader
* interfaces:目标类实现的接口
* invocationHandler:执行处理器
**/
return Proxy.newProxyInstance(ProxyTest.class.getClassLoader(), target.getClass().getInterfaces(), new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
if (method.getName().equals("sayHello")) {
doBefore();
try {
method.invoke(target, args);
} catch (Exception e) {
doThrowing();
}
doAfter();
}
return null;
}
});
}
private static void doThrowing() {
System.out.println("AOP say throw a exception");
}
private static void doBefore() {
System.out.println("AOP before say");
}
private static void doAfter() {
System.out.println("AOP after say");
}
}
源码分析:
/**
* 生成代理类
*/
@Override
public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {
Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);
for (Class<?> intf : interfaces) {
/*
* 1、验证接口Class存在,通过Class.forName()方法获取interfaceClass,再比对intf;
* 2、验证intf为接口:if (!interfaceClass.isInterface()){}
* 3、验证intf非重复实现接口:if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) {}
*/
String proxyPkg = null; // 定义代理类所属包
int accessFlags = Modifier.PUBLIC | Modifier.FINAL;
/*
* 获取所有interfaceClass所属包,要求所有interfaceClass同属一个包,否则报错IllegalArgumentException
* 定义代理类proxyPkg为此包
*/
proxyPkg = pkg;
/*
* 按序生成代理类名称,通过自增编号命名,AtomicLong.getAndIncrement()原子操作,确保编号唯一
*/
long num = nextUniqueNumber.getAndIncrement();
String proxyName = proxyPkg + proxyClassNamePrefix + num;
/*
* 关键步骤:根据名称、接口Class生成指定代理类字节组;
* 通过native方法defineClass0()获取代理类Class
*/
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
proxyName, interfaces, accessFlags);
try {
return defineClass0(loader, proxyName,
proxyClassFile, 0, proxyClassFile.length);
} catch (ClassFormatError e) {
throw new IllegalArgumentException(e.toString());
}
}
}
暂缺
核心差异是:Spring AOP基于代理模式,在运行时(runtime)时为业务类建立代理类,再通过代理类实现实现对业务方法的调用拦截,从而实现织入(weaving);而AspectJ则在编译(compile)过程中进行织入,AJ修改了业务代码的结构树从而实现切面编程。(待参考源码核实)。
基于这个核心差异,Spring AOP与AspectJ在应用上以下区别:
Spring AOP切入点仅支持Spring Beans;而AspectJ可用于基于普通Java对象的切面模块化。
例子:
<beans>
<!-- 被代理对象 -->
<bean id="driver" class="com.spring.aop.cutbean.Driver"></bean>
<!-- 通知类 -->
<bean id="advices" class="com.spring.aop.xmlconfig.Advices"></bean>
<!-- aop配置 -->
<aop:config proxy-target-class="true">
<!--切面 -->
<aop:aspect ref="advices">
<!-- 切点,所有public方法均为切入点 -->
<aop:pointcut expression="execution(public *.*(..))" id="pointcut1"/>
<!--连接通知方法与切点 -->
<aop:before method="before" pointcut-ref="pointcut1"/>
<aop:after method="after" pointcut-ref="pointcut1"/>
</aop:aspect>
</aop:config>
</beans>
public class Program {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("cutbean.xml");
Driver driver = ctx.getBean("driver", Driver.class);
driver.drivingCar();
}
}
public class Advices {
public void before(JoinPoint jp) {
System.out.println("----------前置通知----------");
System.out.println(jp.getSignature().getName());
}
public void after(JoinPoint jp) {
System.out.println("----------最终通知----------");
}
}
public class Driver {
public void drivingCar() {
this.startEngine(); //通过this调用对象持有方法
// driving car
}
public void startEngine() {
// try to start Engine
}
}
打印结果:
----------前置通知----------
drivingCar
----------最终通知----------
切入点表达式为expression="execution(public *.*(..))"
,所有public方法均可以被切入,观察以上结果,startEngine()方法并未切入,证明this
调用对象本身为Java对象非Spring Beans对象情况下,Spring AOP无法为其创建代理实现切入。
补充:可以通过AopContext.currentProxy()获取对象本身的代理类实现,如下:
public class Driver {
public void drivingCar() {
//直接调用this本身方法,Spring AOP无法切入
//this.startEngine();
//通过AopContext获取当前对象的代理类,通过代理调用对象的方法,Spring AOP可以切入
((Driver) AopContext.currentProxy()).startEngine();
}
public void startEngine() {
// try to start engine
}
}
打印结果:
----------前置通知----------
drivingCar
----------前置通知----------
startEngine
----------最终通知----------
----------最终通知----------
根据前文结束可知,Spring是通过配合使用JDK动态代理及CGLIB工具库实现AOP,当目标切入方法非接口方法时,Spring通过CGLIB**创建目标类的子类**,再调父类的目标方法实现AOP。但是,一旦目标父类使用了关键字final,子类无法继承,切入就不能实现。
什么是final类:使用final来修饰的类叫作final类。final类通常功能是完整的,它们不能被继承。Java中有许多类是final的,譬如String, Interger以及其他包装类。下面是final类的实例:
final class PersonalLoan{}
class CheapPersonalLoan extends PersonalLoan{ //compilation error: cannot inherit from final class
将前文例子Driver
类改为final
报错:
public final class Driver { ... }
//运行报错:
//Caused by: java.lang.IllegalArgumentException: Cannot subclass final class com.spring.aop.cutbean.Driver
因此,无法在使用final修饰的bean上应用横切关注点。
SpringBoot定义切面方法:
注意通过@Component注解注入Spring上下文
通过@Pointcue定义切点
@Component
@Aspect
public class WebControllerAop {
//指定切点
//匹配com.zkn.learnspringboot.web.controller包及其子包下的所有类的所有方法
@Pointcut("execution(* com.zkn.learnspringboot.web.controller..*.*(..))")
public void executeService(){
}
}
内容项 | 示例 |
---|---|
任意公共方法的执行 | execution(public * *(..)) |
任何一个以“set”开始的方法的执行 | execution(* set*(..)) |
CustomerService接口的任意方法的执行 | execution(* com.tg.service.CustomerService.*(..)) |
定义在service包里的任意方法的执行 | execution(* com.tg.service..(..)) |
定义在service包和所有子包里的任意类的任意方法的执行 | execution(* com.xyz.service...(..)) |
定义在pointcutexp包和所有子包里的JoinPointObjP2类的任意方法的执行 | execution(* com.test.spring.aop.pointcutexp..JoinPointObjP2.*(..))") |
在多个表达式之间使用or 表示或,使用&& 、and 表示与,! 表示 非 |
aop:pointcut expression="(execution(* com.travelsky.ccboy.dao...find(..))) or (execution(* com.travelsky.ccboy.dao...query(..)))" |
配置前置通知:
/**
* 前置通知,方法调用前被调用
* @param joinPoint
*/
@Before("executeService()")
public void doBeforeAdvice(JoinPoint joinPoint){
System.out.println("我是前置通知!!!");
//获取目标方法的参数信息
Object[] obj = joinPoint.getArgs();
//AOP代理类的信息
joinPoint.getThis();
//代理的目标对象
joinPoint.getTarget();
//用的最多 通知的签名
Signature signature = joinPoint.getSignature();
//代理的是哪一个方法
System.out.println(signature.getName());
//AOP代理类的名字
System.out.println(signature.getDeclaringTypeName());
//AOP代理类的类(class)信息
signature.getDeclaringType();
//获取RequestAttributes
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
//从获取RequestAttributes中获取HttpServletRequest的信息
HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
//如果要获取Session信息的话,可以这样写:
//HttpSession session = (HttpSession) requestAttributes.resolveReference(RequestAttributes.REFERENCE_SESSION);
Enumeration<String> enumeration = request.getParameterNames();
Map<String,String> parameterMap = Maps.newHashMap();
while (enumeration.hasMoreElements()){
String parameter = enumeration.nextElement();
parameterMap.put(parameter,request.getParameter(parameter));
}
String str = JSON.toJSONString(parameterMap);
if(obj.length > 0) {
System.out.println("请求的参数信息为:"+str);
}
}
注意:这里用到了JoinPoint和RequestContextHolder。通过JoinPoint可以获得通知的签名信息,如目标方法名、目标方法参数信息等。通过RequestContextHolder来获取请求信息,Session信息。
接下来我们在Controller类里添加一个请求处理方法来测试一下前置通知:
配置后置返回通知的代码如下:
/**
* 后置返回通知
* 这里需要注意的是:
* 如果参数中的第一个参数为JoinPoint,则第二个参数为返回值的信息
* 如果参数中的第一个参数不为JoinPoint,则第一个参数为returning中对应的参数
* returning 限定了只有目标方法返回值与通知方法相应参数类型时才能执行后置返回通知,否则不执行,对于returning对应的通知方法参数为Object类型将匹配任何目标返回值
* @param joinPoint
* @param keys
*/
@AfterReturning(value = "execution(* com.zkn.learnspringboot.web.controller..*.*(..))",returning = "keys")
public void doAfterReturningAdvice1(JoinPoint joinPoint,Object keys){
System.out.println("第一个后置返回通知的返回值:"+keys);
}
@AfterReturning(value = "execution(* com.zkn.learnspringboot.web.controller..*.*(..))",returning = "keys",argNames = "keys")
public void doAfterReturningAdvice2(String keys){
System.out.println("第二个后置返回通知的返回值:"+keys);
}