java SPI 07 - Automatically generate SPI profile implementation

Series Catalog

What is spi 01-spi?Getting Started

spi 02-spi battle resolution slf4j package conflict problem

spi 03-spi jdk for source code parsing

spi 04-spi dubbo for source code resolution

spi 05-dubbo adaptive extension adaptive expansion

spi 06 - Implement SPI framework from scratch

spi 07-Automatically generate SPI profile implementation

review

In the last section, we implemented a simple version of the SPI by ourselves.

Together, we implement a tool similar to google auto.

Use Demo

Class implementation

  • Say.java

Define interfaces

@SPI
public interface Say {

    void say();

}
  • SayBad.java
@SPIAuto("bad")
public class SayBad implements Say {

    @Override
    public void say() {
        System.out.println("bad");
    }

}
  • SayGood.java
@SPIAuto("good")
public class SayGood implements Say {

    @Override
    public void say() {
        System.out.println("good");
    }

}

Execution effect

After executing mvn clean install.

Automatically generate files under META-INF/services/folderCom.github.houbb.Spi.bs.spi.Say

The contents are as follows:

good=com.github.houbb.spi.bs.spi.impl.SayGood
bad=com.github.houbb.spi.bs.spi.impl.SayBad

code implementation

This section is mainly used for compile-time annotations, which are relatively difficult.

All sources are open source at lombok-ex

Annotation Definition

@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.TYPE})
@Documented
public @interface SPIAuto {

    /**
     * Alias
     * @return Alias
     * @since 0.1.0
     */
    String value() default "";

    /**
     * Target Folder
     * @return Folder
     * @since 0.1.0
     */
    String dir() default "META-INF/services/";

}

In fact, here dir() can not be exposed, here later want to do more flexible expansion, so tentatively.

Core implementation

@SupportedAnnotationTypes("com.github.houbb.lombok.ex.annotation.SPIAuto")
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class SPIAutoProcessor extends BaseClassProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        java.util.List<LClass> classList = super.getClassList(roundEnv, getAnnotationClass());
        Map<String, Set<String>> spiClassMap = new HashMap<>();

        for (LClass lClass : classList) {
            String spiClassName = getSpiClassName(lClass);

            String fullName = lClass.classSymbol().fullname.toString();
            if(StringUtil.isEmpty(spiClassName)) {
                throw new LombokExException("@SPI class not found for class: "
                        + fullName);
            }
            Pair<String, String> aliasAndDirPair = getAliasAndDir(lClass);
            String newLine = aliasAndDirPair.getValueOne()+"="+fullName;

            // Full path: folder + interface name
            String filePath = aliasAndDirPair.getValueTwo()+spiClassName;

            Set<String> lineSet = spiClassMap.get(filePath);
            if(lineSet == null) {
                lineSet = new HashSet<>();
            }
            lineSet.add(newLine);
            spiClassMap.put(filePath, lineSet);
        }

        // Generate Files
        generateNewFiles(spiClassMap);

        return true;
    }
}

Overall process:

(1) Traverse through all classes to find classes with SPIAuto annotations

(2) Grouping all classes into SPI interfaces based on class information and annotation information and storing them in map

(3) Generate the corresponding profile information based on the information in the map.

Get SPI interface method name

Get all the interfaces of the current class, and find the first interface to return labeled with @SPI.

/**
 * Get the corresponding spi class
 * @param lClass Class Information
 * @return Result
 * @since 0.1.0
 */
private String getSpiClassName(final LClass lClass) {
    List<Type> typeList =  lClass.classSymbol().getInterfaces();
    if(null == typeList || typeList.isEmpty()) {
        return "";
    }
    // Get the value for the comment
    SPIAuto auto = lClass.classSymbol().getAnnotation(SPIAuto.class);
    for(Type type : typeList) {
        Symbol.ClassSymbol tsym = (Symbol.ClassSymbol) type.tsym;
        //TOOD: Add an extension here later.
        if(tsym.getAnnotation(SPI.class) != null) {
            return tsym.fullname.toString();
        }
    }
    return "";
}

Get annotation information

Notes are designed to be more flexible and relatively simple, as follows:

Aliases for classes default to lowercase class names, similar to spring.

private Pair<String, String> getAliasAndDir(LClass lClass) {
    // Get the value for the comment
    SPIAuto auto = lClass.classSymbol().getAnnotation(SPIAuto.class);
    //1. Alias
    String fullClassName = lClass.classSymbol().fullname.toString();
    String simpleClassName = fullClassName.substring(fullClassName.lastIndexOf("."));
    String alias = auto.value();
    if(StringUtil.isEmpty(alias)) {
        alias = StringUtil.firstToLowerCase(simpleClassName);
    }
    return Pair.of(alias, auto.dir());
}

Generate Files

Generating files is the core hunger part, referring primarily to google's auto Realization:

In fact, the main difficulty lies in the path acquisition of the file, which is more cumbersome in compile-time annotations, resulting in redundant code writing.

/**
 * Create a new file
 * key: File Path
 * value: Corresponding Content Information
 * @param spiClassMap Target File Path
 * @since 0.1.0
 */
private void generateNewFiles(Map<String, Set<String>> spiClassMap) {
    Filer filer = processingEnv.getFiler();
    for(Map.Entry<String, Set<String>> entry : spiClassMap.entrySet()) {
        String fullFilePath = entry.getKey();
        Set<String> newLines = entry.getValue();
        try {
            // would like to be able to print the full path
            // before we attempt to get the resource in case the behavior
            // of filer.getResource does change to match the spec, but there's
            // no good way to resolve CLASS_OUTPUT without first getting a resource.
            FileObject existingFile = filer.getResource(StandardLocation.CLASS_OUTPUT, "",fullFilePath);
            System.out.println("Looking for existing resource file at " + existingFile.toUri());
            Set<String> oldLines = readServiceFile(existingFile.openInputStream());
            System.out.println("Looking for existing resource file set " + oldLines);
            // Write in
            newLines.addAll(oldLines);
            writeServiceFile(newLines, existingFile.openOutputStream());
            return;
        } catch (IOException e) {
            // According to the javadoc, Filer.getResource throws an exception
            // if the file doesn't already exist.  In practice this doesn't
            // appear to be the case.  Filer.getResource will happily return a
            // FileObject that refers to a non-existent file but will throw
            // IOException if you try to open an input stream for it.
            // File does not exist
            System.out.println("Resources file not exists.");
        }
        try {
            FileObject newFile = filer.createResource(StandardLocation.CLASS_OUTPUT, "",
                    fullFilePath);
            try(OutputStream outputStream = newFile.openOutputStream();) {
                writeServiceFile(newLines, outputStream);
                System.out.println("Write into file "+newFile.toUri());
            } catch (IOException e) {
                throw new LombokExException(e);
            }
        } catch (IOException e) {
            throw new LombokExException(e);
        }
    }
}

Other

That's the whole idea, and there are some details that won't expand here.

Welcome to github lombok-ex.

If it helps you, give a star a boost to the author~

A Reflection on Progress

As part of the framework, ecology is primarily intended to facilitate users.

This tool is actually more flexible, such as automatically generating spi profiles for Dubbo spis.

Reference material

AutoServiceProcessor

Tags: Java github Dubbo Lombok

Posted on Thu, 18 Jun 2020 14:27:32 -0400 by ares