Java元编程和热更新技术总结


元编程

元编程则是能生成代码的代码。

它能在编译时或运行时加入自定义功能,赋予程序更多的灵活性。在我的概念中,Java中的元编程很大程度上属于模板模式

自省

自省使代码有了解自身结构的能力,能够在特定的位置访问代码中的属性或者方法。

反射

Java的反射功能主要在java.lang.refelet包,Classjava.lang包。

反射把Java的类、方法和属性等代码的组成元素抽象为类,让程序人员可以直接操纵代码组成元素,而不是操作类本身。

Class


动态代理

实现在java.lang.refelet包。动态代理让代码在运行时选择一个合适的实现来完成任务。

参见


Bean描述符

实现在java.lang.beans包。

能够以描述符的方式解构按照Java Bean规范写的POJO。很多地方类似于反射,但仅限于Bean。


Method Handle

实现在java.lang.invoke包。在JVM层面的支持是invokevirtual命令;常量池增加了3中类型:CONSTANT_MethodHandle_infoCONSTANT_MethodType_infoCONSTANT_InvokeDynamic_info。MethodHandle使用的是signature polymorphism而不是type descriptor

MethodType

构造MethodType时要提供要调用方法的参数列表和返回类型,以供MethodHandle使用。通常MethodType是由静态方法构造,它的两个私有构造方法要求提供返回参数和参数列表。

A MethodType represents the arguments and return type accepted and returned by a method handle or passed and expected by a method handle caller.

public final class MethodType implements java.io.Serializable {
    private MethodType(Class<?> rtype, Class<?>[] ptypes, boolean trusted) {
        checkRtype(rtype);
        checkPtypes(ptypes);
        this.rtype = rtype;
        this.ptypes = trusted ? ptypes : Arrays.copyOf(ptypes, ptypes.length);
    }

    private MethodType(Class<?>[] ptypes, Class<?> rtype) {
        this.rtype = rtype;
        this.ptypes = ptypes;
    }
}

MethodHandle

MethodHandle是对底层可执行方法的引用,有了MethodType就可以获取MethodHandle。

Method handles are dynamically and strongly typed according to their parameter and return types. They are not distinguished by the name or the defining class of their underlying methods.

触发方法时可以调用以下几个方法:

  • invoke:会尝试在调用的时候进行返回值和参数类型的转换。
  • invokeExact:和invoke不同在于类型必须要完全一致,参数列表和返回类型不可以有转型。
  • invokeWithArguments

也可以绑定到具体对象上去执行:

  • bindTo

MethodHandles.Lookup

工具类,以下方法可以获得MethodHandle,各自对应了JVM命令:

  • findStatic:invokestatic
  • findVirtual:invokevirtual&invokeinterface
  • findSpecial:invokespecial

lookup


函数式接口

Function

可以把函数方法作为参数传递,使Java获得了一些动态语言特性。在JVM层面的支持是invokevirtual命令。

function_interface


编译时

Annotation Processing

实现在javax.annotationjavax.lang.model。是javac的一个工具,能在编译时扫描和处理注解,会启动一个单独的JVM环境来执。Java提供了一个抽象实现:

public abstract class AbstractProcessor implements Processor {
    /**
     * 返回Elements, Types和Filer等工具类。
     */
    public synchronized void init(ProcessingEnvironment env) {
    }

    /**
     * 注解处理器处理主要逻辑的方法。
     */
    public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) {
    }

    /**
     * 返回注解处理器支持的注解。
     * 
     * @return
     */
    public Set<String> getSupportedAnnotationTypes() {
    }

    /**
     * 返回支持的Java版本。
     */
    public SourceVersion getSupportedSourceVersion() {
    }
}

javax.lang.model包含把Java源码解构为一系列元素、或把一系列元素组装为Java源码的的功能,是专门为Annotation Processing服务的,类似于反射

Element


加载时

Instrumentation

实现在java.lang.instrument

Provides services that allow Java programming language agents to instrument programs running on the JVM.

Instrumentation的具体实现依赖于JVMTI。Java Virtual Machine Tool Interface(JVMTI)是一套由 Java 虚拟机提供的,为 JVM 相关的工具提供的本地编程接口集合。可以在命令行启动时加入以下参数:

-javaagent:jarpath[=options]

JAR文件中的manifest必须要包含实现了premain方法的类;或者是包含agentmain方法的类。比如:

Manifest-Version: 1.0 
Premain-Class: Premain

或者:

Manifest-Version: 1.0 
Agentmain-Class: Agentmain

premain

在JVM启动之后,premain会在main方法之前被调用,JVM会按照以下顺序查找:

public static void premain(String agentArgs, Instrumentation inst);
public static void premain(String agentArgs);

如果有第一个premain方法就不会尝试调用第二个方法。

agentmain

agentmain方法可以在main方法执行之后运行,同样JVM会按照以下顺序查找:

public static void agentmain (String agentArgs, Instrumentation inst);
public static void agentmain (String agentArgs); 

修改字节码

比较常用的就是Spring的AOP了。

AnnotationAwareAspectJAutoProxyCreator

从上图可见,注解方式依赖的AnnotationAwareAspectJAutoProxyCreator是继承自BeanPostProcessor,AOP最终就是在postProcessAfterInitialization方法中实现的。

Spring实现AOP使用两种策略:

  • JDK的动态代理;
  • CGLIB。

分别对应JdkDynamicAopProxyObjenesisCglibAopProxy


SPI

实现在java.util.ServiceLoader

面向接口编程的一个典型案例,分为接口service和接口的实现service provider。使用者可以根据不同的需要使用service provider,只要它实现了service即可,通过在META-INF/services目录中写入用实现类的全限定名命名的文件,告诉程序具体的实现类,通过ServiceLoader去加载:

com.xxx.service.ServiceProvider

ServiceLoader中可见调用load方法寻找实现类时,它直接去META-INF/services目录:

public final class ServiceLoader<S> implements Iterable<S> {
    private static final String PREFIX = "META-INF/services/";
}

在Java中比较常见的应用是SQL的DriverManager中加载数据库驱动的代码,可以看到它是通过ServiceLoader去找数据库的驱动程序:

public class DriverManager {
    // ...
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }

    // ...
    private static void loadInitialDrivers() {
        // ...
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();

                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                }
                return null;
            }
        });
    // ...
}

Spring Boot的自动配置采用了类似的策略:Understanding Auto-configured Beans


热更新

JMX

实现在 java.lang.managementjavax.management包。

JMX

如上图所示,JMX分为3个层次:

Instrumentation

An MBean is a managed Java object, similar to a JavaBeans component, that follows the design patterns set forth in the JMX specification. An MBean can represent a device, an application, or any resource that needs to be managed.

MBean即被管理的资源。JMX定义了5种MBean(后三种缺少文档暂未找到样例):

类型 描述
Standard MBean 由MBean接口和实现组成。管理的资源定义在接口中,类似于Java Bean。接口命名以MBean结尾,如MBean为Hello,则接口必须为HelloMBean。
Dynamic MBean 实现javax.management.DynamicMBean接口,所有属性、方法都在运行时决定。
Open MBean  
Model MBean  
MXBean  

JMX Agent

MBeanServer提供对资源的注册和管理,可以通过java.lang.management.ManagementFactory的静态方法获取。通常使用getPlatformMBeanServer

JVM提供了一些内置的MBeanServer参见

用户自己编写的MBean注册到MBeanService上需要使用ObjectName。它对命名有一定规则:

An ObjectName can be written as a String with the following elements in order:

  • The domain.
  • A colon (:).
  • A key property list as defined below.

Remote Management

提供远程访问的入口。主要有如下三种:

jconsole

直接把MBean注册到MBeanServer上通过jconsole访问。

HtmlAdaptorServer

通过HTML页面访问:

MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();

HtmlAdaptorServer adapter = new HtmlAdaptorServer();
adapterName = new ObjectName("SimpleAgent:name=htmladapter,port=8000");
mbs.registerMBean(adapter, adapterName);

adapter.setPort(8000);
adapter.start();

RMI

将MBean发布到服务中,通过RMI访问。

JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:9999/server"); 
JMXConnectorServer cs = JMXConnectorServerFactory.newJMXConnectorServer(url, null, mbs);
cs.start();

总结

为什么元编程和热更新要放在一起总结?

我个人认为这两件事对于Java来说都属于“魔法”了,赋予了Java一些动态语言的特性,在日常的编码中很少会使用到这些技术。

(写到了12月2日)


参考文献

深入理解Java虚拟机

Package java.lang.invoke

Package java.lang.instrument

Annotation Processing101

Instrumentation 新功能

Understanding Auto-configured Beans

Java SPI思想梳理

Spring AOP 使用介绍,从前世到今生

Spring AOP 源码解析

JMX超详细解读

Getting Started with Java Management Extensions (JMX): Developing Management and Monitoring Solutions

Java Management Extensions (JMX)