字节码插桩(四) | AST

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 我们通过 AndroidStudio 生成Bean对象一般是通过注解来实现自动生成getter/setter方法、equals()和hashCode()方法,其中类(或接口)要符合驼式命名法,首字母大写。方法要符合驼式命名法,首字母小写,类或实例变量要符合驼式命名法,首字母小写。常量要求全部由大写字母或下划线构成,且第一个字符不能是下划线,否则编译器会报警告

改不完的 Bug,写不完的矫情。公众号 小木箱成长营 现在专注音视频和 APM ,涵盖各个知识领域;只做全网最 Geek 的公众号,欢迎您的关注!

前言

我们通过 AndroidStudio 生成Bean对象一般是通过注解来实现自动生成getter/setter方法、equals()和hashCode()方法,其中类(或接口)要符合驼式命名法,首字母大写。方法要符合驼式命名法,首字母小写,类或实例变量要符合驼式命名法,首字母小写。常量要求全部由大写字母或下划线构成,且第一个字符不能是下划线,否则编译器会报警告

那么: 编译器是怎么解析到这些不规范的命名方式呢?这里不得不提到一个很重要的字节码插桩技术AST,什么是AST?

一. AST概念

AST 是 Abstract Syntax Tree 的缩写,即 "抽象语法树", 是编译器对代码第一步工作加工后的结果,是一个树形表示的源代码。源代码的每一个元素映射到一个节点或子节树

二. Java编译过程

了解AST之前要了解整个Java编译过程:大概分为如下三个阶段

1681569744261.png

2.1 第一阶段

所有源文件会被解析成语法树

2.2 第二阶段

调用注解处理器,即APT模块。如果注解处理器产生了新的源文件,新的源文件也要参与编译

2.3 第三阶段

语法树会被分析转换成类文件

三. AST原理

编译器对代码处理流程大概是

javaTXT -> 词语法分析 -> 生成AST -> 语义分析 -> 编译字节码

用过操作AST,可以达到修改源代码功能

四. 代码实现层面 APT + AST

  • 4.1 通过 AnnotationProcessorprocess 方法, 通过拿到所有 Elements 对象
  • 4.2 自定义 TreeTranslator,在 visitMethodDef 可对方法进行判断
  • 4.3 如果是目标方法,通过 AST 框架有关 API 插入代码

五. AST缺陷

  • 5.1 不支持Lambda
  • 5.2 APT无法扫描其他moudle,AST无法处理其他moudle

六. AST 运用场景: 代码规范检查

  • 6.1 对象调用的非空判断
  • 6.2 编写我们特定的语法规则,对不符合规则的代码进行修改或优化
  • 6.3 增删改查

1681569787200.png

七. AST优点

AST操作属于编译器级别,对程序运行完全没有影响,效率相对其他AOP更高

八. AST常见API

image.png

  • 抽象内部类,内部定义了访问各种语法节点的方法,获取到对应的语法节点后我们可以对语法节点增加删除或者修改语句;
  • Visitor派生子类有TreeScanner(扫描所有的语法节点)和 TreeTranslator(扫描节点且可以把语法节点转换成另一种语法节点)

九. Android 中 AST的 运用

Android Lint是Google提供给Android开发者的静态代码检查工具。内部基于已经帮我们AST封装了一层,使用Lint对Android工程代码进行扫描和检查,可以发现代码潜在的问题,提醒程序员及早修正。这样代码看起来就没那么难受,今天我就带大家打造一款自己企业项目的Lint工具吧

十. 开发步骤

需求点:

  • 日志输出

禁止 Log 和 System.out日志输出,防止部分隐私数据泄露

  • Toast

禁止直接使用系统Toast,保证样式以及低版本room上面的奔溃概率

  • 文件命名检测

资源文件(layout、drawble、anim、color、dime、style、string)的命名必须以module打头

  • Thread检测

避免自己新建线程

  • 序列化检测

1681569846685.png

10.1 创建Java工程,配置Gradle

1681569885537.png

apply plugin: 'java-library'
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    // lint-api: 官方给出的API,API并不是最终版,官方提醒随时有可能会更改API接口
    compileOnly 'com.android.tools.lint:lint-api:27.1.0'
    // lint-checks:已有的检查
    compileOnly 'com.android.tools.lint:lint-checks:27.1.0'
}
tasks.withType(JavaCompile) {
    options.encoding = "UTF-8"
}
sourceCompatibility = "8"
targetCompatibility = "8"

10.2 创建Detector

Detector负责扫描代码,发现问题并报告

10.2.1 Id类型检查
/**
 * Created by 杨正友 on 2020-01-10
 * Id类型检查:数字类型的id只能被定义为long,不能被定义为int、char或者short
 *
 * @since 1.0
 */
public class IdCheckDetector extends Detector implements Detector.UastScanner {
    private static final String REGEX = ".*id";
    public static final Issue ISSUE = Issue.create(
            "IdDefinedError",
            "Id of number type should be defined long not int.",
            "Please change the Id to long!",
            Category.CORRECTNESS, 9, Severity.ERROR,
            new Implementation(IdCheckDetector.class, Scope.JAVA_FILE_SCOPE)
    );
    @Nullable
    @Override
    public List<Class<? extends UElement>> getApplicableUastTypes() {
        return Arrays.asList(UClass.class);
    }
    @Nullable
    @Override
    public UElementHandler createUastHandler(@NotNull JavaContext context) {
        return new UElementHandler() {
            @Override
            public void visitClass(@NotNull UClass node) {
                // 当前类检查
                check(node, context);
                UClass[] innerClasses = node.getInnerClasses();
                for (UClass uClass : innerClasses) {
                    // 内部类检查
                    check(uClass, context);
                }
                super.visitClass(node);
            }
        };
    }
    private void check(@NotNull UClass node, @NotNull JavaContext context) {
        for (UField field : node.getFields()) {
            String name = field.getName();
            if (name != null && name.toLowerCase().matches(REGEX)
                    && (field.getType() == PsiType.INT || field.getType() == PsiType.CHAR || field.getType() == PsiType.SHORT)) {
                context.report(ISSUE, context.getLocation(field), "Id of number type should be defined long not int.");
            }
        }
    }
}
10.2.2 message.obtain()检查
public class MessageObtainDetector extends Detector implements Detector.UastScanner {
    private static final Class<? extends Detector> DETECTOR_CLASS = MessageObtainDetector.class;
    private static final EnumSet<Scope> DETECTOR_SCOPE = Scope.JAVA_FILE_SCOPE;
    public static final Issue ISSUE = Issue.create(
            "MessageObtainUseError",
            "不建议直接new Message()",
            "建议调用{handler.obtainMessage} or {Message.Obtain()}获取缓存的message",
            Category.PERFORMANCE,
            9,
            Severity.WARNING,
            new Implementation(DETECTOR_CLASS, DETECTOR_SCOPE)
    );
    @Nullable
    @Override
    public List<String> getApplicableConstructorTypes() {
        return Collections.singletonList("android.os.Message");
    }
    @Override
    public void visitConstructor(JavaContext context, UCallExpression node, PsiMethod constructor) {
        context.report(ISSUE, node, context.getLocation(node), "建议调用{handler.obtainMessage} or {Message.Obtain()}获取缓存的message");
    }
}
10.2.3 避免自己创建Thread
public class MkThreadDetector extends Detector implements Detector.UastScanner {
    public static final Issue ISSUE = Issue.create(
            "新建Thread",
            "避免自己创建Thread",
            "请勿直接调用new Thread(),建议使用统一的MkThreadManager",
            Category.PERFORMANCE, 5, Severity.ERROR,
            new Implementation(NewThreadDetector.class, Scope.JAVA_FILE_SCOPE));
    @Override
    public List<String> getApplicableConstructorTypes() {
        return Collections.singletonList("java.lang.Thread");
    }
    @Override
    public void visitConstructor(JavaContext context, UCallExpression node, PsiMethod constructor) {
        context.report(ISSUE, node, context.getLocation(node), "禁止直接调用new Thread(),建议使用MkThreadManager");
    }
}
10.2.4 序列化内部类检查
public class MkSerializableDetector extends Detector implements Detector.UastScanner {
    private static final String CLASS_SERIALIZABLE = "java.io.Serializable";
    public static final Issue ISSUE = Issue.create(
            "InnerClassSerializable",
            "内部类需要实现Serializable接口",
            "内部类需要实现Serializable接口",
            Category.SECURITY, 5, Severity.ERROR,
            new Implementation(SerializableDetector.class, Scope.JAVA_FILE_SCOPE));
    @Nullable
    @Override
    public List<String> applicableSuperClasses() {
        return Collections.singletonList(CLASS_SERIALIZABLE);
    }
    /**
     * 扫描到applicableSuperClasses()指定的list时,回调该方法
     */
    @Override
    public void visitClass(JavaContext context, UClass declaration) {
        if (declaration instanceof UAnonymousClass) {
            return;
        }
        sortClass(context, declaration);
    }
    private void sortClass(JavaContext context, UClass declaration) {
        for (UClass uClass : declaration.getInnerClasses()) {
            sortClass(context, uClass);
            // 判断是否继承了Serializable并提示
            boolean hasImpled = false;
            for (PsiClassType psiClassType : uClass.getImplementsListTypes()) {
                if (CLASS_SERIALIZABLE.equals(psiClassType.getCanonicalText())) {
                    hasImpled = true;
                    break;
                }
            }
            if (!hasImpled) {
                context.report(ISSUE,
                        uClass.getNameIdentifier(),
                        context.getLocation(uClass.getNameIdentifier()),
                        String.format("内部类 `%1$s` 需要实现Serializable接口", uClass.getName()));
            }
        }
    }
}
10.2.5 禁用系统Log/System.out日志
public class MkLogDetector extends Detector implements Detector.UastScanner {
    private static final String SYSTEM_SERIALIZABLE = "System.out.println";
    public static final Issue ISSUE = Issue.create(
            "LogUse",
            "禁止使用Log/System.out.println",
            "推荐MKLog,防止在正式包打印log",
            Category.SECURITY, 5, Severity.ERROR,
            new Implementation(LogDetector.class, Scope.JAVA_FILE_SCOPE));
    @Nullable
    @Override
    public List<String> getApplicableConstructorTypes() {
        return Collections.singletonList(SYSTEM_SERIALIZABLE);
    }
       @Override
    public List<String> getApplicableMethodNames() {
        //要检测的方法名
        return Arrays.asList("v", "d", "i", "w", "e", "wtf");
    }
    @Override
    public void visitConstructor(JavaContext context, UCallExpression node, PsiMethod constructor) {
        context.report(ISSUE, node, context.getLocation(node), "请使用MkLog,避免使用System.out.println");
    }
}

10.3 创建Android Library工程 mk_lintrules

主要用来依赖lint.jar,打包成aar上传到maven,如果没有maven地址可以自己在

gradle中配置

 dependencies {
    lintChecks project(':mk_lint')
}

10.4 IssueRegistry,提供需要被检测的Issue列表

/**
 * Created by 小木箱 on 2020-10-15
 *
 * @since 1.0
 */
public class IssuesRegister extends IssueRegistry {
    @NotNull
    @Override
    public List<Issue> getIssues() {
        return new ArrayList<Issue>() {{
            add(NewThreadDetector.ISSUE);
            add(MessageObtainDetector.ISSUE);
            add(SerializableDetector.ISSUE);
            add(IdCheckDetector.ISSUE);
            add(LogDetector.ISSUE);
        }};
    }
    @Override
    public int getApi() {
        return ApiKt.CURRENT_API;
    }
    @Override
    public int getMinApi() {
        //兼容3.1
        return 1;
    }
}

在getIssues()方法中返回需要被检测的Issue List。

在build.grade中声明Lint-Registry属性

jar {
    manifest {
        attributes("Lint-Registry-v2": "com.github.microkibaco.mk_lint.IssuesRegister")
    }
}

自定义Lint的编码部分就完成了。

十一. Lint的优点

  1. 对于正式发布包来说,debug和verbose的日志会自动不显示。
  2. 拥有更多的有用信息,包括应用程序名字、日志的文件和行信息、时间戳、线程等。
  3. 由于使用了可变参数,禁用后日志的性能比Log高。因为最冗长的日志往往都是debug或verbose日志,这可以稍微提高一些性能。
  4. 可以覆盖日志的写入位置和格式。


相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
存储 监控 Java
字节码插桩(三): ASM 字节码插桩(1)
字节码插桩(三): ASM 字节码插桩
254 0
字节码插桩(三): ASM 字节码插桩(1)
|
存储 JavaScript 安全
深入探索编译插桩(一.JVM原理解析)
作为Android开发者,日常写java,是否想过,玩玩class文件,直接对class文件的字节码下手,我们可以使用class字节码做很多有趣的事情:
|
存储 人工智能 Java
一起来学字节码插桩:从分析class文件结构开始
`Java` 能做到 `一次编译,到处运行`,主要就是靠 `class字节码` 文件,也就是 `java` 文件经过编译之后 `.java -> .class`,然后再被` JVM` 虚拟机加载。其实,不仅是 `java` 语言,只要是符合规则的 `class` 字节码文件,都可以被 `JVM` 加载
135 0
|
Java API 开发工具
字节码插桩(三): ASM 字节码插桩(2)
字节码插桩(三): ASM 字节码插桩(2)
138 0
|
存储 人工智能 Java
通过字节码分析i++ 与 ++i
通过字节码分析i++ 与 ++i
|
Java Linux Android开发
【Groovy】编译时元编程 ( 编译时元编程引入 | 声明需要编译时处理的类 | 分析 Groovy 类的 AST 语法树 )
【Groovy】编译时元编程 ( 编译时元编程引入 | 声明需要编译时处理的类 | 分析 Groovy 类的 AST 语法树 )
167 0
【Groovy】编译时元编程 ( 编译时元编程引入 | 声明需要编译时处理的类 | 分析 Groovy 类的 AST 语法树 )
【Groovy】编译时元编程 ( 编译 ASTTransformation | 打包 ASTTransformation 字节码文件 | 编译 Groovy 类同进行编译时处理 )
【Groovy】编译时元编程 ( 编译 ASTTransformation | 打包 ASTTransformation 字节码文件 | 编译 Groovy 类同进行编译时处理 )
206 0
【Groovy】编译时元编程 ( 编译 ASTTransformation | 打包 ASTTransformation 字节码文件 | 编译 Groovy 类同进行编译时处理 )
|
监控 Java Android开发
【字节码插桩】AOP 技术 ( “字节码插桩“ 技术简介 | AspectJ 插桩工具 | ASM 插桩工具 )
【字节码插桩】AOP 技术 ( “字节码插桩“ 技术简介 | AspectJ 插桩工具 | ASM 插桩工具 )
362 0
【字节码插桩】AOP 技术 ( “字节码插桩“ 技术简介 | AspectJ 插桩工具 | ASM 插桩工具 )
|
自然语言处理 前端开发 Java
JVM13_字节码文件的跨平台、前端编译器、什么是字节码指令(一)
①. 字节码文件的跨平台性(了解) ②. Java的前端编译器(了解)
141 0
JVM13_字节码文件的跨平台、前端编译器、什么是字节码指令(一)
|
数据可视化 前端开发 Java
JVM13_字节码文件的跨平台、前端编译器、什么是字节码指令(二)
③. 透过字节码指令看代码细节 ④. 如何解读供虚拟机解释执行的二进制字节码?
123 0
JVM13_字节码文件的跨平台、前端编译器、什么是字节码指令(二)