您的位置:首页 > 编程语言 > Go语言

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有一个非常好的了解。

使用示例

通过使用来看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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  gradle 源码 hugo