写在前面的话

相关背景及资源:

曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享

曹工说Spring Boot源码(2)-- Bean Definition到底是什么,咱们对着接口,逐个方法讲解

曹工说Spring Boot源码(3)-- 手动注册Bean Definition不比游戏好玩吗,我们来试一下

曹工说Spring Boot源码(4)-- 我是怎么自定义ApplicationContext,从json文件读取bean definition的?

曹工说Spring Boot源码(5)-- 怎么从properties文件读取bean

曹工说Spring Boot源码(6)-- Spring怎么从xml文件里解析bean的

曹工说Spring Boot源码(7)-- Spring解析xml文件,到底从中得到了什么(上)

曹工说Spring Boot源码(8)-- Spring解析xml文件,到底从中得到了什么(util命名空间)

曹工说Spring Boot源码(9)-- Spring解析xml文件,到底从中得到了什么(context命名空间上)

曹工说Spring Boot源码(10)-- Spring解析xml文件,到底从中得到了什么(context:annotation-config 解析)

曹工说Spring Boot源码(11)-- context:component-scan,你真的会用吗(这次来说说它的奇技淫巧)

曹工说Spring Boot源码(12)-- Spring解析xml文件,到底从中得到了什么(context:component-scan完整解析)

工程代码地址 思维导图地址

工程结构图:

概要

本篇已经是spring源码第13篇,前一篇讲了context:component-scan的完整解析,本篇,继续解析context命名空间里的另一个重量级元素:load-time-weaver。它可以解决你用aop搞不定的事情。

大家如果熟悉aop,会知道aop的原理是基于beanPostProcessor的。比如平时,我们会在service类的部分方法上加@transactional,对吧,transactional是基于aop实现的。最终的效果就是,注入到controller层的service,并不是原始的service bean,而是一个动态代理对象,这个动态代理对象,会去执行你的真正的service方法前后,去执行事务的打开和关闭等操作。

aop的限制就在于:被aop的类,需要被spring管理,管理的意思是,需要通过@component等,弄成一个bean。

那,假设我们想要在一个第三方的,没被spring管理的类的一个方法前后,做些aop的事情,该怎么办呢?

一般来说,目前的方法主要是通过修改class文件。

class文件在什么时候才真正生效?答案是:在下面这个方法执行完成后:

public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}

一旦通过上述方法,获取到返回的Class对象后,基本就不可修改了。

那根据这个原理,大致有3个时间节点(第二种包含了2个时间点),对class进行修改:

  1. 编译器织入,比如aspectJ的ajc编译器,假如你自己负责实现这个ajc编译器,你当然可以自己夹带私货,悄悄地往要编译的class文件里,加点料,对不?这样的话,编译出来的class,和java源文件里的,其实是不一致的;

  2. 自己实现classloader,在调用上述的loadClass(String name)时,自己加点料;通俗地说,这就是本课要讲的load-time-weaving,即,加载时织入;

    其中,又分为两种,因为我们知道,classloader去loadClass的时候,其实是分两步的,一个是java代码层面,一个是JVM层面。

    java代码层面:你自定义的classloader,想怎么玩就怎么玩,比如针对传进来的class,获取到其inputStream后,对其进行修改(增强或进行解密等)后,再丢给JVM去加载为一个Class;

    JVM层面:Instrumentation机制,具体理论的东西我也说不清,简单来说,就是java命令启动时,指定agent参数,agent jar里,有一个premain方法,该方法可以注册一个字节码转换器。

    字节码转换器接口大致如下:

    public interface ClassFileTransformer {
    // 这个方法可以对参数中指定的那个class进行转换,转换后的class的字节码,通过本方法的返回参数返回
    // 即,本方法的返回值,就是最终的class的字节码
    byte[]
    transform( ClassLoader loader,
    String className,
    Class<?> classBeingRedefined,
    ProtectionDomain protectionDomain,
    byte[] classfileBuffer)
    throws IllegalClassFormatException;
    }

    大家参考下面两篇文章。

    Java Instrumentation,这一篇原文没代码,我自己整理了下,附上了具体的步骤,放在码云

    参考文章2

第一种,需要使用aspectj的编译器来进行编译,还是略显麻烦;这里我们主讲第二种,LTW。

LTW其实,包含了两部分,一部分是切面的问题(切点定义切哪儿,通知定义在切点处要嵌进去的逻辑),一部分是切面怎么生效的问题。

我们下面分别来讲。

Aspectj的LTW怎么玩

我们可以参考aspectj的官网说明:

https://www.eclipse.org/aspectj/doc/released/devguide/ltw-configuration.html

这里面提到了实现ltw的三种方式,其中第一种,就是我们前面说的java instrumentation的方式,只是这里的agent是使用aspectjweaver.jar;第二种,使用了专有命令来执行,这种方式比较奇葩,直接跳过不理;第三种,和我们前面说的类似,就是自定义classloader的方式:

Enabling Load-time Weaving

AspectJ 5 supports several ways of enabling load-time weaving for an application: agents, a command-line launch script, and a set of interfaces for integration of AspectJ load-time weaving in custom environments.

  • Agents

    AspectJ 5 ships with a number of load-time weaving agents that enable load-time weaving. These agents and their configuration are execution environment dependent. Configuration for the supported environments is discussed later in this chapter.Using Java 5 JVMTI you can specify the -javaagent:pathto/aspectjweaver.jar option to the JVM.Using BEA JRockit and Java 1.3/1.4, the very same behavior can be obtained using BEA JRockit JMAPI features with the -Xmanagement:class=org.aspectj.weaver.loadtime.JRockitAgent

  • Command-line wrapper scripts aj

    The aj command runs Java programs in Java 1.4 or later by setting up WeavingURLClassLoader as the system class loader. For more information, see aj.The aj5 command runs Java programs in Java 5 by using the -javaagent:pathto/aspectjweaver.jar option described above. For more information, see aj.

  • Custom class loader

    A public interface is provided to allow a user written class loader to instantiate a weaver and weave classes after loading and before defining them in the JVM. This enables load-time weaving to be supported in environments where no weaving agent is available. It also allows the user to explicitly restrict by class loader which classes can be woven. For more information, see aj and the API documentation and source for WeavingURLClassLoader and WeavingAdapter.

第一种方式呢,我这里弄了个例子,代码放在:

https://gitee.com/ckl111/spring-boot-first-version-learn/tree/master/all-demo-in-spring-learning/java-aspectj-agent

整个demo的代码结构如下图:

  1. 目标类,是要被增强的对象

    package foo;
    
    public class StubEntitlementCalculationService {
    
        public void calculateEntitlement() {
    System.out.println("calculateEntitlement");
    }
    }
  2. 切面类

    package foo;
    
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Pointcut; @Aspect
    public class ProfilingAspect { @Around("methodsToBeProfiled()")
    public Object profile(ProceedingJoinPoint pjp) throws Throwable {
    System.out.println("before");
    try {
    return pjp.proceed();
    } finally {
    System.out.println("after");
    }
    } // 这里定义了切点
    @Pointcut("execution(public * foo..*.*(..))")
    public void methodsToBeProfiled(){}
    }
  3. aop配置,指定要使用的切面,和要扫描的范围

    <!DOCTYPE aspectj PUBLIC "-//AspectJ//DTD//EN" "https://www.eclipse.org/aspectj/dtd/aspectj.dtd">
    <aspectj> <weaver>
    <!-- only weave classes in our application-specific packages -->
    <include within="foo.*"/>
    </weaver> <aspects>
    <!-- weave in just this aspect -->
    <aspect name="foo.ProfilingAspect"/>
    </aspects> </aspectj>
  4. 测试类

    package foo;
    
    public final class Main {
    
        public static void main(String[] args) {
    StubEntitlementCalculationService entitlementCalculationService = new StubEntitlementCalculationService();
    // 如果进展顺利,这处调用会被增强
    entitlementCalculationService.calculateEntitlement();
    }
    }
  5. 启动测试

    执行步骤:
    1.mvn clean package,得到jar包:java-aspectj-agent-1.0-SNAPSHOT 2.把aspectjweaver-1.8.2.jar拷贝到和本jar包同路径下 3.cmd下执行:
    java -javaagent:aspectjweaver-1.8.2.jar -cp java-aspectj-agent-1.0-SNAPSHOT.jar foo.Main

    执行的效果如下:

Aspectj的LTW的原理剖析

我们这一小节,简单说说其原理。我们前面提到,aspectj的ltw共三种方式,我们上面用了第一种,这种呢,其实就是基于instrumentation机制来的。

只是呢,这里我们指定的agent是aspectj提供的aspectjweaver.jar。我这里把这个jar包(我这里版本是1.8.2)解压缩了一下,我们来看看。

解压缩后,在其META-INF/MANIFEST.MF中,我们看到了如下内容:

Manifest-Version: 1.0
Name: org/aspectj/weaver/
Specification-Title: AspectJ Weaver Classes
Specification-Version: 1.8.2
Specification-Vendor: aspectj.org
Implementation-Title: org.aspectj.weaver
Implementation-Version: 1.8.2
Implementation-Vendor: aspectj.org
Premain-Class: org.aspectj.weaver.loadtime.Agent 这个地方重点关注,这个是指定main执行前要执行的类
Can-Redefine-Classes: true

上面我们看到,其指定了:

Premain-Class: org.aspectj.weaver.loadtime.Agent

那么我们看看这个类:

/**
* Java 1.5 preMain agent to hook in the class pre processor
* Can be used with -javaagent:aspectjweaver.jar
* */
public class Agent { /**
* The instrumentation instance
*/
private static Instrumentation s_instrumentation; /**
* The ClassFileTransformer wrapping the weaver
*/
private static ClassFileTransformer s_transformer = new ClassPreProcessorAgentAdapter(); /**
* JSR-163 preMain Agent entry method
* 敲黑板,这个premain的方法签名是定死了的,和我们main方法类似。其中,参数instrumentation是由JVM传进来的
* @param options
* @param instrumentation
*/
public static void premain(String options, Instrumentation instrumentation) {
/* Handle duplicate agents */
if (s_instrumentation != null) {
return;
}
s_instrumentation = instrumentation;
// 这里,加了一个字节码转换器
s_instrumentation.addTransformer(s_transformer);
} /**
* Returns the Instrumentation system level instance
*/
public static Instrumentation getInstrumentation() {
if (s_instrumentation == null) {
throw new UnsupportedOperationException("Java 5 was not started with preMain -javaagent for AspectJ");
}
return s_instrumentation;
} }

别的我也不多说,多的我也不懂,只要大家明白,这里premain会在main方法执行前执行,且这里的instrumentation由JVM传入,且这里通过执行:

s_instrumentation.addTransformer(s_transformer);

给JVM注入了一个字节码转换器。

这个字节码转换器的类型是,ClassPreProcessorAgentAdapter。

这个类里面呢,翻来覆去,代码很复杂,但是大家想也知道,无非是去aop.xml文件里,找到要使用的Aspect切面。切面里面定义了切点和切面逻辑。拿到这些后,就可以对目标class进行转换了。

我大概翻了代码,解析aop.xml的代码在:org.aspectj.weaver.loadtime.ClassLoaderWeavingAdaptor类中。

	// aop文件的名称
private final static String AOP_XML = "META-INF/aop.xml"; /**
* 加载aop.xml
* Load and cache the aop.xml/properties according to the classloader visibility rules
*
* @param loader
*/
List<Definition> parseDefinitions(final ClassLoader loader) { List<Definition> definitions = new ArrayList<Definition>();
try {
String resourcePath = System.getProperty("org.aspectj.weaver.loadtime.configuration", AOP_XML); StringTokenizer st = new StringTokenizer(resourcePath, ";"); while (st.hasMoreTokens()) {
String nextDefinition = st.nextToken();
... 这里面是具体的解析
}
}
...
return definitions;
}

AspectJ的LTW的劣势

优势我就不多说了,大家可以自由发挥,比如大家熟知的性能监控啥的,基本都是基于这个来做的。

劣势是啥?大家发现了吗,我们总是需要在启动时,指定-javaagent参数,就像下面这样:

java -javaagent:aspectjweaver-1.8.2.jar -cp java-aspectj-agent-1.0-SNAPSHOT.jar foo.Main

大概有以下问题:

  • 很多时候,部署是由运维去做的,开发不能做到只给一个jar包,还得让运维去加参数,要是运维忘了呢?风险很大;
  • 假设我们要进行ltw的是一个tomcat的webapp应用,但这个tomcat同时部署了好几个webapp,但是另外几个webapp其实是不需要被ltw的,但是么办法啊,粒度就是这么粗。

基于以上问题,出现了spring的基于aspectJ进行了优化的,粒度更细的LTW。

具体我下节再讲。

总结

本来是打算讲清楚spring的context:load-time-weaver,无奈内容太多了,只能下节继续。今天内容到这,谢谢大家。源码我是和spring这个系列放一块的,其实今天的代码比较独立,大家可以加我,我单独发给大家也可以。

曹工说Spring Boot源码(13)-- AspectJ的运行时织入(Load-Time-Weaving),基本内容是讲清楚了(附源码)的更多相关文章

  1. 曹工说Spring Boot源码系列开讲了(1)-- Bean Definition到底是什么,附spring思维导图分享

    写在前面的话&&About me 网上写spring的文章多如牛毛,为什么还要写呢,因为,很简单,那是人家写的:网上都鼓励你不要造轮子,为什么你还要造呢,因为,那不是你造的. 我不是要 ...

  2. 曹工说Spring Boot源码(14)-- AspectJ的Load-Time-Weaving的两种实现方式细细讲解,以及怎么和Spring Instrumentation集成

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

  3. 曹工说Spring Boot源码(15)-- Spring从xml文件里到底得到了什么(context:load-time-weaver 完整解析)

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

  4. 曹工说Spring Boot源码(16)-- Spring从xml文件里到底得到了什么(aop:config完整解析【上】)

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

  5. 曹工说Spring Boot源码(17)-- Spring从xml文件里到底得到了什么(aop:config完整解析【中】)

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

  6. 曹工说Spring Boot源码(18)-- Spring AOP源码分析三部曲,终于快讲完了 (aop:config完整解析【下】)

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

  7. 曹工说Spring Boot源码(19)-- Spring 带给我们的工具利器,创建代理不用愁(ProxyFactory)

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

  8. 曹工说Spring Boot源码(20)-- 码网灰灰,疏而不漏,如何记录Spring RedisTemplate每次操作日志

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

  9. 曹工说Spring Boot源码(21)-- 为了让大家理解Spring Aop利器ProxyFactory,我已经拼了

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

随机推荐

  1. 使用element的upload组件实现一个完整的文件上传功能(下)

    本篇文章是<使用element的upload组件实现一个完整的文件上传功能(上)>的续篇. 话不多说,接着上一篇直接开始 一.功能完善—保存表格中每一列的文件列表状态 1.思路 保存表格中 ...

  2. Redis 都有哪些数据类型?分别在哪些场景下使用比较合适?

    redis 主要有以下几种数据类型: string hash list set sorted set string 这是最简单的类型,就是普通的 set 和 get,做简单的 KV 缓存. set c ...

  3. The third day of Crawler learning

    连续爬取多页数据 分析每一页url的关联找出联系 例如虎扑 第一页:https://voice.hupu.com/nba/1 第二页:https://voice.hupu.com/nba/2 第三页: ...

  4. ulimit -u 解决 Jenkins OOM 错误

    Apr 24, 2018 11:19:48 AM hudson.init.impl.InstallUncaughtExceptionHandler$DefaultUncaughtExceptionHa ...

  5. 【证明与推广与背诵】Matrix Tree定理和一些推广

    [背诵手记]Matrix Tree定理和一些推广 结论 对于一个无向图\(G=(V,E)\),暂时钦定他是简单图,定义以下矩阵: (入)度数矩阵\(D\),其中\(D_{ii}=deg_i\).其他= ...

  6. 洛谷$P4040\ [AHOI2014/JSOI2014]$宅男计划 贪心

    正解:三分+贪心 解题报告: 传送门$QwQ$ 其实很久以前的寒假就考过了,,,但那时候$gql$没有好好落实,就只写了个二分,并没有二分套三分,就只拿到了$70pts$ #include <b ...

  7. $Noip2013/Luogu1970$ 花匠 $dp$+思维

    $Luogu$ $Sol$ 和$Poj1037\ A\ Decorative\ Fence$好像吖. $f[i][0/1]$表示前$i$个数,且选了第$i$个数,这个数相对于上一个数是下降(上升)的, ...

  8. Ural1057. Amount of Degrees 题解 数位DP

    题目链接: (请自行百度进Ural然后查看题号为1057的那道题目囧~) 题目大意: Create a code to determine the amount of integers, lying ...

  9. 小小知识点(二十六)关于5G发展的28个核心问题,来自华为内部的深度解读

    本文来自微信公众号“腾讯深网”(ID:qqshenwang),作者 马关夏.36氪经授权转载. 一.5G先进性与行业应用 1.5G到底是什么?和4G比有什么不一样? 从国际电信联盟(ITU)的定义来看 ...

  10. 6.6 hadoop作业调优

    提高速度和性能.可以从下面几个点去优化 可以在本地运行调试来优化性能,但是本地和集群是完全不同的环境,数据流模式也截然不同,性能优化要在集群上测试.有些问题如(内存溢出)只能在集群上重现. HPROF ...