Hugo源码分析
2016-11-05 22:41
330 查看
同时发表在: http://blog.houzhi.me/2016/11/05/hugo-sourcecode-analysis
Hugo是JakeWharton大神开发的一个通过注解触发的Debug日志库。它是一个非常好的AOP框架,在Debug模式下,Hugo利用aspectj库来进行切面编程,插入日志代码。通过分析Hugo的代码能够对gradle以及aspectj有一个非常好的了解。
首先把下面的编译配置文件添加到项目当中:
使用的时候直接使用@DebugLog注解给想要调试的方法就好了,它会打印函数的参数,执行时间,以及返回值:
输出:
需要指出的,Hugo只会在Debug模式下打印log。DebugLog的源码如下:
就是注解,Target也包含了TYPE,DebugLog也可以添加到类上面:
现在我们想要给test()方法增加一些东西,比如打印test方法进入的时间。如果用aspectj则可以增加一个文件:
通过执行命令 ajc -d . TestService.java LogAspect.java生成TestService.class,然后执行命令java TestService,输出为:
实际上将ajc理解为类似于javac的编译工具就好了,它编译的目标跟javac一样的都是java class文件,只是源文件的语法是符合aspect语法的。可以看看TestService.class反编译后的源码:
将aspect理解编译时期对源文件按照指定的描述(aspect语法文件)进行编译,得到进行切入后的字节码文件。详细的介绍可以参看这篇文章spring aop。
我们看看Hugo项目中是如何使用aspect,实际的aspect部分代码是在hugo-runtime子模块当中。
上面只是给出的代码去除了相关详细内容,具体代码可以直接看源码Hugo.java。
上面是使用了aspect注解来描述相关插入代码的:
@Aspect 表示这个类由AspectJ处理
@Pointcut 描述切面内容,可以理解为针对哪些方法,类,进行拦截,插入代码。
@Around 实际上拦截方法,这个注解可以同时拦截方法的执行前后,另外有@Before, @After,顾名思义,表示方法执行前跟方法执行后拦截。
关于aspectj,邓凡平的这篇文章深入理解Android之AOP介绍的挺详细的。aspectj编译器会根据这些描述信息对项目中的源码进行插入。另外还有cglib能够有类似的功能,CGlib是在运行期对类进行动态代理(Proxy.newProxyInstance只能对接口进行动态代理),具体可以Google一下。
我们使用过程的方式是:
指定了插件实现的代码。然后看hugo.weaving.plugin.HugoPlugin的内容(对应hugo-plugin/src/main/groovy/hugo/weaving/plugin/HugoPlugin.groovy文件),这是groovy源文件,groovy也是一种编程语言,跟Java差不多。关于gradle插件声明使用可以参看gradle源码目录下面的samples/customPlugin项目(比如~/.gradle/wrapper/dists/gradle-2.10-all/a4w5fzrkeut1ox71xslb49gst/gradle-2.10/samples)。HugoPlugin.groovy的文件内容如下:
上面这个HogoPlugin.groovy指定了编译的时候区分Debug和release版本编译。非Debug并且没有disable Hugo的时候,使用aspect给应用中的代码插入。aspect会找到有@Aspect注解的类,然后解析这个类,处理代码。这样整个过程就完了。
mavenCentral()和jcenter是指定了repositories,也就是远程仓库,gradle编译的时候,可以从这些仓库里面获取引用库的包。
org.aspectj:aspectjtools : 是aspectj的库
com.github.dcendents:android-maven-gradle-plugin: 修改自maven插件,是一个让maven与android库(arr)相兼容的库。
gradle-nexus-plugin: 是配置和上传组件的gradle插件
build.gradle里面定义了几个Task,我们看一下cleanExample的定义来简单了解一下Task:
Task cleanExample 是清理example项目的task,上述代码使用gradlew脚本,将工作目录设置为hugo-example目录下面,设置gradle的参数为clean,这样就清理hugo-example项目了。
这里只是简单介绍一下gradle,如果不了解gradle,建议先看看gradle的介绍文档,比如Gradle for Android中文,另外就是邓凡平的深入理解Android(一):Gradle详解。
我觉得对于gradle,正确的理解方式是它是基于groovy脚本的一种构建框架,它提供了Android编译的框架及其API。另外groovy种充满了闭包,理解好闭包的概念,然后查看API,这样入手和理解Gradle会很容易明白。
hugo-annotations-release里面使用SOURCE retention的DebugLog。这样就能够在debug版本使用CLASS retention的DebugLog,而release版本使用SOURCE retention的DebugLog。不过这种配置只能适合debug和release打包时。如果有更好地idea,我在hugo上面提了个issues:support release build type use DebugLog with SOURCE Retention ,直接评论。
参考:
Spring AOP,AspectJ, CGLIB 有点晕:http://www.jianshu.com/p/fe8d1e8bd63e
Spring AOP 实现原理与 CGLIB 应用:https://www.ibm.com/developerworks/cn/java/j-lo-springaopcglib/
深入理解Android之AOP:http://blog.csdn.net/innost/article/details/49387395
Hugo 探究:https://yq.aliyun.com/articles/7104
Gradle for Android中文:https://avatarqing.gitbooks.io/gradlepluginuserguidechineseverision/content/introduction/README.html
深入理解Android(一):Gradle详解:http://www.infoq.com/cn/articles/android-in-depth-gradle
Hugo是JakeWharton大神开发的一个通过注解触发的Debug日志库。它是一个非常好的AOP框架,在Debug模式下,Hugo利用aspectj库来进行切面编程,插入日志代码。通过分析Hugo的代码能够对gradle以及aspectj有一个非常好的了解。
使用示例
通过使用来看Hugo具体的功能,这样也能够更好的明白Hugo的实现方式。首先把下面的编译配置文件添加到项目当中:
buildscript { repositories { mavenCentral() } dependencies { classpath 'com.jakewharton.hugo:hugo-plugin:1.2.1' } } apply plugin: 'com.android.application' apply plugin: 'com.jakewharton.hugo'
使用的时候直接使用@DebugLog注解给想要调试的方法就好了,它会打印函数的参数,执行时间,以及返回值:
@DebugLog public String getName(String first, String last) { SystemClock.sleep(15); // Don't ever really do this! return first + " " + last; }
输出:
V/Example: ⇢ getName(first="Jake", last="Wharton") V/Example: ⇠ getName [16ms] = "Jake Wharton"
需要指出的,Hugo只会在Debug模式下打印log。DebugLog的源码如下:
package hugo.weaving; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.CONSTRUCTOR; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.CLASS; @Target({TYPE, METHOD, CONSTRUCTOR}) @Retention(CLASS) public @interface DebugLog { }
就是注解,Target也包含了TYPE,DebugLog也可以添加到类上面:
@DebugLog static class Greeter { private final String name; Greeter(String name) { this.name = name; } private String sayHello() { return "Hello, " + name; } }
AspectJ
AspectJ是一个面向切面的框架,它有一个专门的编译器用来生成遵守Java字节编码规范的class文件。在这里下载安装,实际上它有自己的语法。看个简单的例子:// 我们有一个TestService类,TestService.java public class TestService{ public void test(){ System.out.println("test"); } public static void main(String[]args){ new TestService().test(); } }
现在我们想要给test()方法增加一些东西,比如打印test方法进入的时间。如果用aspectj则可以增加一个文件:
public aspect LogAspect { pointcut logPointcut():execution(void TestService.test()); void around():logPointcut(){ System.out.println("start time: " + System.currentTimeMillis()); proceed(); System.out.println("end time: " + System.currentTimeMillis()); }
通过执行命令 ajc -d . TestService.java LogAspect.java生成TestService.class,然后执行命令java TestService,输出为:
start time: 1478071231659 test end time: 1478071231659
实际上将ajc理解为类似于javac的编译工具就好了,它编译的目标跟javac一样的都是java class文件,只是源文件的语法是符合aspect语法的。可以看看TestService.class反编译后的源码:
org.aspectj.runtime.internal.AroundClosureTestService { TestService() { } test() { test_aroundBody1$advice(LogAspect.aspectOf()(AroundClosure))} main(String[] args) { (TestService()).test()} }
将aspect理解编译时期对源文件按照指定的描述(aspect语法文件)进行编译,得到进行切入后的字节码文件。详细的介绍可以参看这篇文章spring aop。
我们看看Hugo项目中是如何使用aspect,实际的aspect部分代码是在hugo-runtime子模块当中。
package hugo.weaving.internal; import android.os.Build; import android.os.Looper; import android.os.Trace; import android.util.Log; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.Signature; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.CodeSignature; import org.aspectj.lang.reflect.MethodSignature; import java.util.concurrent.TimeUnit; @Aspect public class Hugo { private static volatile boolean enabled = true; @Pointcut("within(@hugo.weaving.DebugLog *)") public void withinAnnotatedClass() {} @Pointcut("execution(!synthetic * *(..)) && withinAnnotatedClass()") public void methodInsideAnnotatedType() {} @Pointcut("execution(!synthetic *.new(..)) && withinAnnotatedClass()") public void constructorInsideAnnotatedType() {} @Pointcut("execution(@hugo.weaving.DebugLog * *(..)) || methodInsideAnnotatedType()") public void method() {} @Pointcut("execution(@hugo.weaving.DebugLog *.new(..)) || constructorInsideAnnotatedType()") public void constructor() {} public static void setEnabled(boolean enabled) { Hugo.enabled = enabled; } @Around("method() || constructor()") public Object logAndExecute(ProceedingJoinPoint joinPoint) throws Throwable { enterMethod(joinPoint); long startNanos = System.nanoTime(); Object result = joinPoint.proceed(); long stopNanos = System.nanoTime(); long lengthMillis = TimeUnit.NANOSECONDS.toMillis(stopNanos - startNanos); exitMethod(joinPoint, result, lengthMillis); return result; } private static void enterMethod(JoinPoint joinPoint) { if (!enabled) return; // ... //组织相关信息到builder当中 if (Looper.myLooper() != Looper.getMainLooper()) { builder.append(" [Thread:\"").append(Thread.currentThread().getName()).append("\"]"); } Log.v(asTag(cls), builder.toString()); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { final String section = builder.toString().substring(2); Trace.beginSection(section); } } private static void exitMethod(JoinPoint joinPoint, Object result, long lengthMillis) { if (!enabled) return; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { Trace.endSection(); } // ... //组织相关信息为builder Log.v(asTag(cls), builder.toString()); } private static String asTag(Class<?> cls) { if (cls.isAnonymousClass()) { return asTag(cls.getEnclosingClass()); } return cls.getSimpleName(); } }
上面只是给出的代码去除了相关详细内容,具体代码可以直接看源码Hugo.java。
上面是使用了aspect注解来描述相关插入代码的:
@Aspect 表示这个类由AspectJ处理
@Pointcut 描述切面内容,可以理解为针对哪些方法,类,进行拦截,插入代码。
@Around 实际上拦截方法,这个注解可以同时拦截方法的执行前后,另外有@Before, @After,顾名思义,表示方法执行前跟方法执行后拦截。
关于aspectj,邓凡平的这篇文章深入理解Android之AOP介绍的挺详细的。aspectj编译器会根据这些描述信息对项目中的源码进行插入。另外还有cglib能够有类似的功能,CGlib是在运行期对类进行动态代理(Proxy.newProxyInstance只能对接口进行动态代理),具体可以Google一下。
gradle代码
Hugo源码中除了aspect的使用,我觉得另外就是项目的编译控制了,因为Hugo只会在Debug模式下打印日志,而控制只在Debug模式下打印日志是在编译脚本中实现的。Hugo源码中目录树主要是:|- hugo-plugin |- hugo-runtime |- hugo-annotations |- hugo-example
我们使用过程的方式是:
buildscript { repositories { mavenCentral() } dependencies { classpath 'com.jakewharton.hugo:hugo-plugin:1.2.1' } } apply plugin: 'com.android.application' apply plugin: 'com.jakewharton.hugo'
Gradle 实现Debug插入代码
所以先看com.jakewharton.hugo插件,这个插件的实现是在hugo-plugin当中,hugo-plugin模块的hugo-plugin/src/main/resources/META-INF/gradle-plugins/目录下面有com.jakewharton.hugo.properties,这就表示插件的声明。该文件的内容是:implementation-class=hugo.weaving.plugin.HugoPlugin
指定了插件实现的代码。然后看hugo.weaving.plugin.HugoPlugin的内容(对应hugo-plugin/src/main/groovy/hugo/weaving/plugin/HugoPlugin.groovy文件),这是groovy源文件,groovy也是一种编程语言,跟Java差不多。关于gradle插件声明使用可以参看gradle源码目录下面的samples/customPlugin项目(比如~/.gradle/wrapper/dists/gradle-2.10-all/a4w5fzrkeut1ox71xslb49gst/gradle-2.10/samples)。HugoPlugin.groovy的文件内容如下:
class HugoPlugin implements Plugin<Project> { @Override void apply(Project project) { def hasApp = project.plugins.withType(AppPlugin) def hasLib = project.plugins.withType(LibraryPlugin) if (!hasApp && !hasLib) { throw new IllegalStateException("'android' or 'android-library' plugin required.") } final def log = project.logger final def variants // variants是构造变种版本,为同一个应用创建不同的版本。 if (hasApp) { variants = project.android.applicationVariants } else { variants = project.android.libraryVariants } project.dependencies { // 声明的项目依赖 debugCompile 'com.jakewharton.hugo:hugo-runtime:1.2.2-SNAPSHOT' // TODO this should come transitively debugCompile 'org.aspectj:aspectjrt:1.8.6' compile 'com.jakewharton.hugo:hugo-annotations:1.2.2-SNAPSHOT' } project.extensions.create('hugo', HugoExtension) variants.all { variant -> if (!variant.buildType.isDebuggable()) { // 非Debug情况下,直接返回 log.debug("Skipping non-debuggable build type '${variant.buildType.name}'.") return; } else if (!project.hugo.enabled) { //关闭Hugo log.debug("Hugo is not disabled.") return; } // 使用Hugo的情况下,调用aspect编译,args指定了aspect相关参数。 JavaCompile javaCompile = variant.javaCompile javaCompile.doLast { String[] args = [ "-showWeaveInfo", "-1.5", "-inpath", javaCompile.destinationDir.toString(), "-aspectpath", javaCompile.classpath.asPath, "-d", javaCompile.destinationDir.toString(), "-classpath", javaCompile.classpath.asPath, "-bootclasspath", project.android.bootClasspath.join(File.pathSeparator) ] log.debug "ajc args: " + Arrays.toString(args) MessageHandler handler = new MessageHandler(true); new Main().run(args, handler);// 运行aspect // ... 省略了log运行结果的代码 } } } }
上面这个HogoPlugin.groovy指定了编译的时候区分Debug和release版本编译。非Debug并且没有disable Hugo的时候,使用aspect给应用中的代码插入。aspect会找到有@Aspect注解的类,然后解析这个类,处理代码。这样整个过程就完了。
Gradle与maven
在hugo目录下面有个build.gradle,先看一下dependencies:buildscript { repositories { mavenCentral() jcenter() } dependencies { classpath 'org.gradle.api.plugins:gradle-nexus-plugin:0.7' classpath 'com.android.tools.build:gradle:1.3.1' classpath 'org.aspectj:aspectjtools:1.8.6' classpath 'com.github.dcendents:android-maven-gradle-plugin:1.3' } }
mavenCentral()和jcenter是指定了repositories,也就是远程仓库,gradle编译的时候,可以从这些仓库里面获取引用库的包。
org.aspectj:aspectjtools : 是aspectj的库
com.github.dcendents:android-maven-gradle-plugin: 修改自maven插件,是一个让maven与android库(arr)相兼容的库。
gradle-nexus-plugin: 是配置和上传组件的gradle插件
build.gradle里面定义了几个Task,我们看一下cleanExample的定义来简单了解一下Task:
task cleanExample(type: Exec) { executable = '../gradlew' workingDir = project.file('hugo-example') args = [ 'clean' ] }
Task cleanExample 是清理example项目的task,上述代码使用gradlew脚本,将工作目录设置为hugo-example目录下面,设置gradle的参数为clean,这样就清理hugo-example项目了。
这里只是简单介绍一下gradle,如果不了解gradle,建议先看看gradle的介绍文档,比如Gradle for Android中文,另外就是邓凡平的深入理解Android(一):Gradle详解。
我觉得对于gradle,正确的理解方式是它是基于groovy脚本的一种构建框架,它提供了Android编译的框架及其API。另外groovy种充满了闭包,理解好闭包的概念,然后查看API,这样入手和理解Gradle会很容易明白。
Hugo 改进
因为Hugo当中DebugLog使用的Retention是CLASS类型,所以打包之后,注解还是会存在,这样release的apk包就会增加一些大小。就拿Hugo的example来说,如果DebugLog的Retention是CLASS,release包大概是3476bytes,如果DebugLog的retention是Source的时候,release包是3444bytes。还是能够减少一点包大小的。目前我还没有完全地弄好这个问题,不过如果是导入library的方式使用Hugo的话,可以这样来弄:debugCompile 'com.jakewharton.hugo:hugo-annotations:1.2.2-SNAPSHOT' releaseCompile 'com.jakewharton.hugo:hugo-annotations-release:1.2.2-SNAPSHOT'
hugo-annotations-release里面使用SOURCE retention的DebugLog。这样就能够在debug版本使用CLASS retention的DebugLog,而release版本使用SOURCE retention的DebugLog。不过这种配置只能适合debug和release打包时。如果有更好地idea,我在hugo上面提了个issues:support release build type use DebugLog with SOURCE Retention ,直接评论。
总结
Hugo是一个比较小的项目,但是里面却包含了优秀的思想以及先进的技术。AOP编程,注解的理解,Gradle编译的理解,Aspect的使用,以及gradle-maven在Hugo项目中都用到了,看一下Hugo的源码是学习这些东西的一个非常好的方式。参考:
Spring AOP,AspectJ, CGLIB 有点晕:http://www.jianshu.com/p/fe8d1e8bd63e
Spring AOP 实现原理与 CGLIB 应用:https://www.ibm.com/developerworks/cn/java/j-lo-springaopcglib/
深入理解Android之AOP:http://blog.csdn.net/innost/article/details/49387395
Hugo 探究:https://yq.aliyun.com/articles/7104
Gradle for Android中文:https://avatarqing.gitbooks.io/gradlepluginuserguidechineseverision/content/introduction/README.html
深入理解Android(一):Gradle详解:http://www.infoq.com/cn/articles/android-in-depth-gradle
相关文章推荐
- String转换成Integer源码分析
- VCL源码分析方法论
- Struts-menu源码分析(转贴)
- PC键盘驱动程序源码分析
- TOMCAT源码分析(启动框架)
- BO2k源码分析(二)----命令循环机制
- String转换成Integer源码分析
- VCL源码分析方法论 cg1120(原作)
- JBPM源码分析(二)---acceptToken函数
- TOMCAT源码分析(消息处理)
- 万花谷网页病毒源码分析
- Jive源码分析:tree树形数据结构
- SharpDevelop源码分析 (一、序+基本概念)
- SharpDevelop源码分析 (三、插件系统)
- TOMCAT源码分析(启动框架)
- Linux TCP/IP 协议栈源码分析(一)
- AbstractList源码分析
- quake2源码分析(一)
- U-BOOT介绍以及disk模块源码分析(下)
- TOMCAT源码分析(消息处理)