Java是一种静态显式强类型语言,导致java代码存在大量的模版化代码,一直被人诟病开发效率低.其实java语言本身以及JVM生态提供了大量动态编程技术,可以大量减少模版化代码.本文尝试整理相关的技术.
Java源代码通过编译生成字节码,字节码在运行期被加载到内存中后被真正执行.java在不同阶段都提供了不同的的API支持对源代码和字节码的操作.
- 反射Reflection
Java的反射可以在程序在运行期可以拿到一个对象的所有信息(元数据)。 - 插入式注解API(Pluggable Annotation Processing API)
APT(Annotation Processor Tool)始于JDK1.6实现了JSR 269规范。JSR 269用Annotation Processor在编译期间处理Annotation,通过修改AST实现修饰代码逻辑的效果.Lombok,structMap使用APT实现声明式编程减少模版化代码. - Instrument API & Java Agent
Instrument是jdk1.5开始提供的一个可以修改已加载类文件的类库.Java SE6中由两种应用Instrumentation的方式,premain(命令行)和agentM=main(运行时).java.lang.instrument包的具体实现依赖于JVMTI(Java VirtualMachine Tool Interface).java agent被拥有APM、热部署和实时诊断工具. - 表达式语言Expression language
Java统一表达式语言Java Unified Expression Language(JUEL).JUEL最初包含在JSP 2.1规范JSR-245中,后来成为Java EE 7的一部分,改在JSR-341中定义。主要的开源实现有:OGNL ,MVEL ,SpEL,JUEL,Java Expression Language (JEXL),JEval,Jakarta JXPath 等。 - 脚本引擎Script Engine
Java 6提供对JSR-223规范的全面支持。该规范提供了一种从Java内部执行脚本编写语言的方便、标准的方式,并提供从脚本内部访问Java资源和类的功能。基于JSR-223可以执行javacript和groovy等解释性语言.
编译工具
所有可执行代码都是由源代码(字符串)编译成字节码和最终CPU指令的,如果动态生成源代码字符串,在动态编译成字节码,然后通过定制的classloader,可以实现完全的动态编码.java有以下工具
- JavaC
JavaCompiler是JDK自带的编译器,通常我们使用javac命令执行编译,由于JavaCompiler完全由java语言编写,所有我们完全可以在运行时实现java源码的编译. - Janino
Janino 是一个极小、极快的 开源Java 编译器。Janino 不仅可以像 JavaC 一样将 Java 源码文件编译为字节码文件,还可以编译内存中的 Java 表达式、块、类和源码文件,加载字节码并在 JVM 中直接执行。Janino 同样可以用于静态代码分析和代码操作。Kettle使用Janino框架来实现自定义"Java代码组件"步骤功能. - JavaCC
Java Compiler Compiler是一个语法生成器和语法分析器,可以通过读取一个.jj描述文件来生成一个Java文件。语法层面的翻译而不是“构造”,JavaCC生成的文件一般是不可读的. - Antlr
ANTLR 是一个强大的语法分析器生成工具,可以使来读取、处理、执行或翻译结构化文本和二进制文件。Hibernate使用ANTLR来处理HQL语言。 - AspectJ compiler (ajc)
ajc是Java编译器的扩展(确切地说,它基于Eclipse 3.3的JDT编译器)。AspectJ有一套特定的API,和AJC编译器代替Javac处理编译期静态切面。AspectJ 实际上就是用一种特定语言编写切面,通过自己的语法编译工具 ajc 编译器来编译,生成一个新的代理类,该代理类增强了业务类。AJC像普通Java编译器一样编译所有Java类,然后它将另外编织受方面影响的所有类。由于依赖单独的编译器,在项目构建配置(Maven、Gragle)显得没有那么方便,而且比较容易出现兼容性问题(APT、Lombok)。
Spring 只是使用了与 AspectJ 一样的注解,没有使用 AspectJ 的编译器,转向采用动态代理技术的实现原理来构建 Spring AOP 的内部机制(动态织入),这是与 AspectJ(静态织入)最根本的区别。
源代码操作框架
理论上我们可以手动拼接字符串产生java源文件,但实际操作对编码挑战太大.
-
JavaPoet
Java Poet 也就是 Java 诗人,是大名鼎鼎的 Square 公司开源的 .java 源文件生成库,它提供易于操作的 API 来生成 Java 代码,让开发者脱离"用代码写代码"中繁琐的导包、括号、缩进等问题,更专注于代码生成的逻辑。 -
JavaParser
依赖JavaCC.Javaparser是通过Java文件生成语法树(AST),然后基于这棵语法树进行Java代码的分析和修改。它可以直接再根据语法树,反向生成Java文件。根据这棵语法树,你可以直接分析一个独立的Java文件,即使这个Java文件乱七八糟,不能通过编译,不用在运行时使用反射等功能。配合JavaPoet可以去做一些非常有意思的功能。比如,Javaparser提取Java文件的注释或者注解,然后通过JavaPoet生成一些自动文档(Swagger),或者进行测试用例的自动填充。除了这些,Javaparser可以规定十分严格的语法格式,所以使用它做一个代码审查工具,甚至是做一些代码依赖分析,也是可以的。 -
javaparser2jctree
将Java Parser AST转换到 JDK AST的一个工具 -
Qdox:paul-hammant/qdox
-
SPOON:INRIA/spoon
字节码框架
不论在运行时还是编译时,都可以使用字节码框架实现业务逻辑的修改.
- Java Proxy
Java Proxy 是 JDK 自带的一个代理工具,它允许为实现了一系列接口的类生成代理类。 - CGLIB
CGLIB 诞生于 Java 初期,但不幸的是没有跟上 Java 平台的发展。虽然 CGLIB 本身是一个相当强大的库,但也变得越来越复杂,年代久远,无人维护。鉴于此,导致许多用户放弃了 CGLIB 。 - Byte Buddy
Byte Buddy 提供了一种非常灵活且强大的领域特定语言,通过编写简单的 Java 代码即可创建自定义的运行时类。与此同时,Byte Buddy 还具有非常开放的定制性,能够应付不同复杂度的需求。被著名的框架和工具(例如Mockito,Hibernate,Jackson,Google的Bazel构建系统等)使用 - ASM: ASM被JDK(jdk proxy), spring(CGLIB),Byte Buddy所使用,是最底层的字节码操作工具。
- Javassist
Javassist 的使用对 Java 开发者来说是非常友好的,它使用Java 源代码字符串和 Javassist 提供的一些简单 API ,共同拼凑出用户想要的 Java 类,Javassist 自带一个编译器,拼凑好的 Java 类在程序运行时会被编译成为字节码并加载到 JVM 中。Javassist 库简单易用,而且使用 Java 语法构建类与平时写 Java 代码类似,但是 Javassist 编译器在性能上比不了 Javac 编译器,而且在动态组合字符串以实现比较复杂的逻辑时容易出错。Dubbo 默认使用 Javassist 来进行动态代理的。 - BCEL
BCEL库提供了一系列用于分析、创建、修改Java Class文件的API。就这个库的功能来看,其使用面远不及同胞兄弟们,但是他比较特殊的一点是,它被包含在了原生的JDK中,位于com.sun.org.apache.bcel。
编程思想
本文篇首所述“Java是一种静态显式强类型语言,一直被人诟病开发效率低”。好像静态显示强类型语言问题特别大。其实恰恰相反,正式“静态显式强类型语言”这一设计原则,才确定了java在企业级服务器端应用开发的王者地位。与perl、javascript这样的弱类型动态语言相比,java提供了以下对企业服务器端应用极为关键的特性:
- 确定性
- 鲁棒性
- 进化性
面向对象思想OOP将对象作为编程的基本单位,同时具有属性、行为。静态类型要求对象属性和行为在编译时就不可变化、强类型要求对象不能随意转换。由此编译通过后的代码在运行期具有极高的确定性,减少运行时发生开发期无法预测的行为。确定性同时也会带来鲁棒性,不会由于一些未知场景带来系统轻易崩溃。Java相对模板化的语法带来了另外一个好处就是程序的可读性可维护性的提高,程序进化性较好。不会出现某些动态语言代码写完三天开发人员自己都看不懂自己的代码情况。
所以如果选择以Java为开发语言,主体编程思想还应该以OOP为主,以获得“静态显示强类型语言”的好处。而java提供的动态编程能力应该是由于主体场景之外需要以其他编程思想补充OOP不擅长的场景。
面向切面编程AOP
面向切面编程思想提供了一个非常优秀的逻辑解耦模式,尤其适合解耦技术逻辑和业务逻辑。通过切面声明的方式将技术逻辑织入到技术逻辑中。从而让业务逻辑编码专注于表述业务逻辑,降低认知复杂度。spring正是由于这一思想成为java企业级应用框架事实上的标准。通过注解Annotation声明事务、缓存、依赖注入的技术逻辑,实际执行逻辑则由spring使用动态代理和字节码生成技术实现,spring大大减轻了开发人员日常编码的负担。
Domain-Specific Language( DSL)
Domain-Specific Language( DSL)领域特定语言是指基于某些使用特定场景时定义特定的语法,提供不同于通用语言的表述能力的编程语言.DSL通常是声明式编程风格,即主要描述要做什么,具体怎么做有特定语言编译成通用语言时负责实现.典型的是spring的java config语法,通过一系列的注解声明bean的行为,由spring框架负责实现具体的逻辑.例如@Autowire注解声明需要注入service,具体执行注入的是编译后的java代码.
DSL实际在java生态中起到了巨大的作用,只是日常我们都只以框架的形式在学习这些语法:
- spring config
- bean validation
- JPA
- SQL
- Rule Engine: Drools、ILog
- Flow Engine: Activiti, JBPM
当我们考虑要使用java实现动态编程时,通常就意味着有可能打破java的正式语法,这时必须要一个稳定的可交流的语言规范,这个规范就是我们要处理的特定领域的语言.
代码生成
- 源代码生成
- 模板驱动的代码生成: JSP
- 模型驱动的代码生成: MybatisGenerator
- 字节码生成
- 基于AST的代码生成: hibernate HQL
- 基于元数据的代码生成: java prxoy