preface

Common dynamic method calls use Reflection or bytecode generation techniques. Although the JDK has been optimized for reflection, it still performs poorly in performance-driven scenarios. This article introduces javassist, a programmer-friendly bytecode manipulation library. The performance shown by Benchmark is almost as good as a direct call.

Open source address: Javassist, a quick look at the official introduction:

Javassist makes Java bytecode manipulation easy. It is a class library for editing bytecode in Java. It enables a Java program to define a new class at run time and modify the class file when the JVM loads it. Unlike other similar bytecode editors, Javassist provides two levels of API: source level and bytecode level. If users use source-level apis, they can edit class files without knowing the Java bytecode specification. The entire API is designed using only the Java language vocabulary. You can even specify the inserted bytecode as source text. Javassist compiles it instantly. On the other hand, bytecode level apis allow the user to edit class files directly like any other editor.

To put it bluntly, we can use its API to generate the bytecode we want. Here’s how to use it.

The body of the

generate

The Jar package needs to be imported first. The repository address is:

<dependency>
    <groupId>org.javassist</groupId>
    <artifactId>javassist</artifactId>
    <version>3.23.1 - GA</version>
</dependency>
Copy the code

To demonstrate, let’s create a main function directly to build an object that doesn’t exist.

public class JavassistInvoker {


    public static void main(String[] args) throws Exception {
        // Create the class, which is a singleton object
        ClassPool pool = ClassPool.getDefault();
        // The class we need to build
        CtClass ctClass = pool.makeClass("io.github.pleuvoir.prpc.invoker.Person");

        // Add a field
        CtField field$name = new CtField(pool.get("java.lang.String"), "name", ctClass);
        // Set the access level
        field$name.setModifiers(Modifier.PRIVATE);
        // You can also give an initial value
        ctClass.addField(field$name, CtField.Initializer.constant("pleuvoir"));
        // Generate get/set methods
        ctClass.addMethod(CtNewMethod.setter("setName", field$name));
        ctClass.addMethod(CtNewMethod.getter("getName", field$name));

        // Add a constructor
        // No argument constructor
        CtConstructor cons$noParams = new CtConstructor(new CtClass[]{}, ctClass);
        cons$noParams.setBody("{name = \"pleuvoir\"; }");
        ctClass.addConstructor(cons$noParams);
        // Has a parameter constructor
        CtConstructor cons$oneParams = new CtConstructor(new CtClass[]{pool.get("java.lang.String")}, ctClass);
        // $0=this $1,$2,$3... Represents method parameters
        cons$oneParams.setBody("{$0.name = $1; }");
        ctClass.addConstructor(cons$oneParams);

        // Create a method called print with no arguments and no return value and print name
        CtMethod ctMethod = new CtMethod(CtClass.voidType, "print".new CtClass[]{}, ctClass);
        ctMethod.setModifiers(Modifier.PUBLIC);
        ctMethod.setBody("{System.out.println(name); }");
        ctClass.addMethod(ctMethod);

        // The target directory of the current project
        final String targetClassPath = Thread.currentThread().getContextClassLoader().getResource("").toURI().getPath();

        // Generate a.class filectClass.writeFile(targetClassPath); }}Copy the code

You can see the generated file as follows:

It is important to note that if we use this method to generate classes in large numbers, it may cause memory stress. This is because there is a hash table in the ClassPool to cache the generated CtClass objects. We can clear the cache by calling the CtClass#detach() method.

So far, our file is generated. How do YOU call it? Load through the ClassLoader? And then we look down.

call

Reflection calls

There are two common ways to do this: call the toClass() method or read a.class file.

Modify the above method:

Here you can see that we got the object we expected, but we couldn’t do the cast because our compiler didn’t have it. In other words, it has to be called by reflection.

You can also load the file by reading it:

// If the generated class is not in the classpath, you need to specify where the class loader is loaded, otherwise it will not be loaded. We don't need to set that up here
// pool.appendClassPath(Thread.currentThread().getContextClassLoader().getResource("").toURI().getPath());
final CtClass loadCtClass = pool.get("io.github.pleuvoir.prpc.invoker.Person");
loadCtClass.toClass().newInstance();
Copy the code

Note: toClass should not be called repeatedly.

Both forms are called by reflection, and we don’t have such entities. So how do you complete the transformation? Smart friends must have thought of, that is to define the interface. After the generation into the interface can be directly called.

OK, no more words. Let’s start by defining an interface just to demonstrate implementing dynamic proxies using JavAssist.

Interface call

Before implementing dynamic proxies, let’s review a set of static proxies.

Let’s first define the target interface to be proxied. Wait, this is also the interface to our program’s cast.

public interface HelloService {

	String sayHello(String name);
}
Copy the code

Define the proxy implementation, passing in the interface implementation class.

public class HelloServiceProxy implements  HelloService {

	private HelloService helloService;

	public HelloServiceProxy(a) {}public HelloServiceProxy(HelloService helloService) {
		this.helloService = helloService;
	}


	// This will be modified
	@Override
	public String sayHello(String name) {
		System.out.println("Static proxy before..");
		helloService.sayHello(name);
		System.out.println("Static proxy after..");
		returnname; }}Copy the code

Very simple, let’s test it:

public class ProxyTest {


    public static void main(String[] args) {

        final HelloServiceProxy serviceProxy = new HelloServiceProxy(new HelloService() {
            @Override
            public String sayHello(String name) {
                System.out.println("Target interface implementation: name=" + name);
                return "null"; }}); serviceProxy.sayHello("pleuvoir"); }}Copy the code

The output is:

Static proxy before.. Target interface implementation: name=pleuvoir Static proxy after..Copy the code

B: Good, as we expected. The next step is to start generating real dynamic proxies.

Consider for a moment that our static proxy is passing in the actual implementation class using the argument constructor. We still have to use reflection if our dynamically generated class calls the constructor, so we add a new interface to set the implementation class. As follows:

public interface IProxy {

    void setProxy(Object t);
}

Copy the code

We expect the generated class to look like this:

public class HelloSericeProxyV2 implements IProxy.HelloService {

    private HelloService helloService;

    public HelloSericeProxyV2(a) {}@Override
    public void setProxy(Object t) {
        this.helloService = (HelloService) t;
    }

    // This will be modified
    @Override
    public String sayHello(String name) {
        System.out.println("Static proxy before..");
        helloService.sayHello(name);
        System.out.println("Static proxy after..");
        returnname; }}Copy the code

The implementation class is set up by calling setProxy, and then the target method sayHello is called. Once you get an object, you can convert it to these two interfaces for method calls. You may ask why void setProxy(Object T) doesn’t use generics. The reason was that it wasn’t supported very well and it was a bit of a hassle to set up, so I didn’t set it up. Take a look at the implementation:

public class ProxyTest {


    public static void main(String[] args) throws Exception {

// final HelloServiceProxy serviceProxy = new HelloServiceProxy(new HelloService() {
// @Override
// public String sayHello(String name) {
// system.out. println(" target interface implementation: name=" + name);
// return "null";
/ /}
/ /});
//
// serviceProxy.sayHello("pleuvoir");

        // Create the class, which is a singleton object
        ClassPool pool = ClassPool.getDefault();
        pool.appendClassPath(Thread.currentThread().getContextClassLoader().getResource("").toURI().getPath());
        // The class we need to build
        CtClass ctClass = pool.makeClass("io.github.pleuvoir.prpc.invoker.HelloServiceJavassistProxy");
        // What interfaces does this class implement

        ctClass.setInterfaces(new CtClass[]{
                pool.getCtClass("io.github.pleuvoir.prpc.invoker.HelloService"),
                pool.getCtClass("io.github.pleuvoir.prpc.invoker.IProxy")});

        // Add a field
        CtField field$name = new CtField(pool.get("io.github.pleuvoir.prpc.invoker.HelloService"), "helloService", ctClass);
        // Set the access level
        field$name.setModifiers(Modifier.PRIVATE);
        ctClass.addField(field$name);

        // Add a constructor
        // No argument constructor
        CtConstructor cons$noParams = new CtConstructor(new CtClass[]{}, ctClass);
        cons$noParams.setBody("{}");
        ctClass.addConstructor(cons$noParams);

        // Rewrite the sayHello method by constructing a string
        CtMethod m = CtNewMethod.make(buildSayHello(), ctClass);
        ctClass.addMethod(m);


        Create a method called setProxy
        CtMethod ctMethod = new CtMethod(CtClass.voidType, "setProxy".new CtClass[]{pool.getCtClass("java.lang.Object")}, ctClass);
        ctMethod.setModifiers(Modifier.PUBLIC);
        // // $0=this $1,$2,$3... Represents method parameters
        ctMethod.setBody("{$0.helloService = $1; }");
        ctClass.addMethod(ctMethod);

        ctClass.writeFile(Thread.currentThread().getContextClassLoader().getResource("").toURI().getPath());

        // Get the instance object
        final Object instance = ctClass.toClass().newInstance();

        System.out.println(Arrays.toString(instance.getClass().getDeclaredMethods()));
        // Set the target method
        if (instance instanceof IProxy) {
            IProxy proxy = (IProxy) instance;
            proxy.setProxy(new HelloService() {
                @Override
                public String sayHello(String name) {
                    System.out.println("Target interface implementation: name=" + name);
                    return "null"; }}); }if (instance instanceof HelloService) {
            HelloService service = (HelloService) instance;
            service.sayHello("pleuvoir"); }}private static String buildSayHello(a) {
        String methodString = " public String sayHello(String name) {\n"
                + "System.out.println(\" Static proxy before.. \ "); \n"
                + " helloService.sayHello(name); \n"
                + "System.out.println(\" Static proxy after.. \ "); \n"
                + " return name; \n"
                + "}";
        returnmethodString; }}Copy the code

The generated.class is what we expect.

reference

  • javassist tutorial
  • Javassist uses full parsing

After the language

OK, we can see that JavAssist is still very convenient and efficient in generating code. Note that using generics is not as cumbersome as possible, and if the resulting bytecode will be cached by other objects you can create it using New ClassPool(true) rather than using the classpool.getDefault () singleton.