文章
问答
冒泡
制作一个基于Freemarker生成sdk代码组件
前言
        最近在给公司做个sdk 服务组件,其中提供给第三方平台的jar包部分,由我制定规范,业务根据规范来进行相应的sdk client端jar包代码编写,本着能省力就省力的原则,我想基于freemarker来制作一个代码生成模块,可以自动生成client端所需代码, 省去人工编写部分.

一、前期工作

1、maven坐标

由于服务端是基于spring-boot生成,所以我就只使用了freemarker的starter,并且这里只作为演示,所以基础简单的来.
<dependency>
     <groupId>org.springframework.boot</groupId> 
    <artifactId>spring-boot-starter-web</artifactId> 
</dependency> 

<dependency> 
    <groupId>org.springframework.boot</groupId> 
    <artifactId>spring-boot-starter-freemarker</artifactId> 
</dependency>

2、ftl文件准备

freemarker是模版引擎,我们提前准备ftl文件,来根据ftl文件模版来生成我们的代码.
0
在resources下新建codegen目录分别对应创建 client用来作为请求类, proxy作为代理类, pojo作为出参入参模版.内容分别如下:

(1)、Client模版

package ${packagePath};

import com.dane.sdk.core.BaseResponse;
import com.dane.sdk.core.BaseClient;
<#if importList?? && (importList?size > 0) >
    <#list importList as importClass>
import ${importClass};
    </#list>
</#if>
public class ${className}Client extends BaseClient<${className}Proxy> implements ${className}Proxy {

    <#if methodList?? && (methodList?size > 0) >
        <#list methodList as method>
           public ${method.resultType} ${method.methodName} (<#if method.params?? && (method.params?size > 0)><#list method.params as param>${param.paramType} ${param.paramName}</#list></#if>) {
                return proxy.${method.methodName}(<#if method.params?? && (method.params?size > 0)><#list method.params as param>${param.paramName}</#list></#if>);
            }

        </#list>
    </#if>

}

(2)、pojo模版

package ${packagePath};

<#if (request)>
import com.dane.sdk.core.BaseRequest;
</#if>

public class ${className} <#if (request)>extends BaseRequest</#if> {

    <#if fields?? && (fields?size > 0) >
        <#list fields as field>
            private ${field.fieldType} ${field.fieldName};

        </#list>
    </#if>

    <#if fields?? && (fields?size > 0) >
        <#list fields as fieldGetSet>
            public ${fieldGetSet.fieldType} get${fieldGetSet.methodName}(){
                return ${fieldGetSet.fieldName};
            }

            public void set${fieldGetSet.methodName}(${fieldGetSet.fieldType} ${fieldGetSet.fieldName}){
                this.${fieldGetSet.fieldName} = ${fieldGetSet.fieldName};
            }
        </#list>
    </#if>
}

(3)、Proxy模版

package ${packagePath};

import com.dane.sdk.core.BaseResponse;
<#if importList?? && (importList?size > 0) >
    <#list importList as importClass>
import ${importClass};
    </#list>
</#if>

public interface ${className}Proxy {

        <#if methodList?? && (methodList?size > 0) >
        <#list methodList as method>
           ${method.resultType} ${method.methodName}(<#if method.params?? && (method.params?size > 0)><#list method.params as param>${param.paramType} ${param.paramName}</#list></#if>);

        </#list>
        </#if>
}

3、扫描方法编写

自上而下的扫描来说,整体的扫描从类->方法->入参、出参

(1)、扫描注解标定

                首先确定需要扫描类的范围,我这里作为示范,定义里@OpenApi 和 @OpenMethod两个注解,分别作用于类和方法上, 用来扫描这两个注解标记的类和方法,确定生成的client.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RestController
public @interface OpenApi {
}
 
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OpenMethod {
}

(2)、扫描类

                这里我的逻辑为,扫描整个指定包前缀的类,然后进行反射获取整个映射类的method,为后续的操作提供第一层的过滤.
private CodeGen scanOpenMethodOrOpenApi(){
    ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
    try {
        String pattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
                ClassUtils.convertClassNameToResourcePath(PATTERN_PATH) + PATTERN_CONTR;
        Resource[] resources = resourcePatternResolver.getResources(pattern);
        //MetadataReader 的工厂类
        MetadataReaderFactory readerfactory = new CachingMetadataReaderFactory(resourcePatternResolver);
        Set<String> importList = Sets.newHashSet();
        List<CodeGenMethod> codeGenMethodList = Lists.newArrayList();
        for (Resource resource : resources) {
            //用于读取类信息
            MetadataReader reader = readerfactory.getMetadataReader(resource);
            //扫描到的class
            String classname = reader.getClassMetadata().getClassName();
            Class<?> clazz = null;
            try {
                clazz = Class.forName(classname);
            }catch (ClassNotFoundException e) {}
            if (clazz == null) {
                continue;
            }
            //判断是否有指定注解
           if (clazz.getAnnotation(OpenApi.class) == null &&
                   clazz.getAnnotation(RestController.class) == null &&
                   clazz.getAnnotation(Controller.class) == null) {
                continue;
           }

           Method[] methods = clazz.getDeclaredMethods();
           if (methods == null || methods.length == 0) {
               continue;
           }
           codeGenMethodList.addAll(scanMethod(methods,importList));
        }
        if (codeGenMethodList == null || codeGenMethodList.isEmpty()) {
            return null;
        }
        CodeGen codegen = CodeGen.builder().build();
        codegen.setMethodList(codeGenMethodList);
        codegen.setImportList(importList);
        codegen.setClassName(applicationName.substring(0,1).toUpperCase()+applicationName.substring(1));
        codegen.setPackagePath(packagePath);
        return codegen;
    } catch (IOException e) {
        e.printStackTrace();
    }
    return null;
}

(3)、扫描方法

                这里我的操作为,扫描上面过滤后的类的所有方法,然后进行第二次过滤, 注意这里 如果直接在controller中使用Lambda表达式或者其他的有代码生成的插件的话,则会误识别出很多无用的方法,所以这里我使用method.isSynthetic()来判断是否为原生方法,
private List<CodeGenMethod> scanMethod(Method[] methods, Set<String> importList){
    List<CodeGenMethod> methodList = Lists.newArrayList();
    for (Method method : methods) {
        // 过滤代码生成方法
        if (method.isSynthetic()) {
            continue;
        }
        if (method.getDeclaringClass().getAnnotation(OpenApi.class) == null) {
            if (method.getAnnotation(OpenMethod.class) == null) {
                continue;
            }
        }
        List<CodeGenPojo> codeGenPojos = Lists.newArrayList();
        // 扫描入参
        Parameter[] parameters = method.getParameters();
        List<CodeGenMethodParam> codeGenMethodParams = Lists.newArrayList();
        for (Parameter parameter : parameters) {
            codeGenPojos.addAll(scanParameter(parameter.getParameterizedType()));
            List<Class> genericClassArray = ReflectionUtil.getGenericClassArray(parameter.getParameterizedType());
            Collections.reverse(genericClassArray);
            StringBuilder paramType = new StringBuilder();
            for (int i = 0; i < genericClassArray.size(); i++) {
                importList.add(genericClassArray.get(i).getName());
                    if (i == 0) {
                        paramType.append(genericClassArray.get(i).getSimpleName());
                        continue;
                    }
                    paramType.insert(0,genericClassArray.get(i).getSimpleName()+"<").append(">");
            }
            codeGenMethodParams.add(CodeGenMethodParam.builder().paramName(parameter.getName())
                    .paramType(paramType.toString()).build());
        }

        // 扫描出参
        Type genericReturnType = method.getGenericReturnType();
        if (genericReturnType != null) {
            codeGenPojos.addAll(scanParameter(genericReturnType));
            List<Class> genericClassArray = ReflectionUtil.getGenericClassArray(genericReturnType);
            if (genericClassArray != null && !genericClassArray.isEmpty()) {
                Collections.reverse(genericClassArray);
                StringBuilder returnType = new StringBuilder();
                for (int i = 0; i < genericClassArray.size(); i++) {
                    importList.add(genericClassArray.get(i).getName());
                    if (i == 0) {
                        returnType.append(genericClassArray.get(i).getSimpleName());
                        continue;
                    }
                    returnType.insert(0,genericClassArray.get(i).getSimpleName()+"<").append(">");
                }
                methodList.add(CodeGenMethod.builder().methodName(method.getName())
                        .params(codeGenMethodParams)
                        .codeGenPojoList(codeGenPojos)
                        .resultType(returnType.toString()).build());
            }
            continue;
        }
        methodList.add(CodeGenMethod.builder().methodName(method.getName())
                .params(codeGenMethodParams)
                .resultType("void").build());
    }
    return methodList;
}

(4)、扫描出参入参

private List<CodeGenPojo> scanParameter(Type type){
    if (type == null) {
        return null;
    }

    List<CodeGenPojo> codeGenPojos = Lists.newArrayList();
    List<Class> genericClassArray = ReflectionUtil.getGenericClassArray(type);
    for (Class clazz : genericClassArray) {
        if (clazz == BaseResponse.class) {
            continue;
        }
        if (ReflectionUtil.isCollection(clazz)) {
           continue;
        }
        if (copyOnWriteArraySet.contains(clazz.getSimpleName())) {
            continue;
        }
        // 生成类字段信息
        CodeGenPojo codeGenPojo = codeGenField(clazz);
        if (codeGenPojo != null) {
            codeGenPojos.add(codeGenPojo);
        }
    }

    for (CodeGenPojo codeGenPojo : codeGenPojos) {
        copyOnWriteArraySet.add(codeGenPojo.getClassName());
    }
    return codeGenPojos;
}

(5)、扫描类字段

private CodeGenPojo codeGenField(Class classType){
    if (classType == null) {
        return null;
    }
    Field[] fields = classType.getDeclaredFields();
    List<CodeGenField> codeGenFields = Lists.newArrayList();
    for (Field field : fields) {
        Class<?> declaringClass = field.getDeclaringClass();
        // 获取范型
        Class fieldGenericClass = ReflectionUtil.getGenericClass(declaringClass);
        if (fieldGenericClass == null) {
            // 没有声明范型, 直接生成字段
            // 如果是容器类型
            if (ReflectionUtil.isCollection(declaringClass)) {
                codeGenFields.add(CodeGenField.builder().fieldName(field.getName()).fieldType(field.getType().getSimpleName() + "<" + declaringClass.getSimpleName() + ">").methodName(field.getName().substring(0, 1).toUpperCase() + field.getName().substring(1)).build());
                continue;
            }
            // 如果是基础类型或者其他类型
            codeGenFields.add(CodeGenField.builder().fieldName(field.getName()).fieldType(field.getType().getSimpleName()).methodName(field.getName().substring(0, 1).toUpperCase() + field.getName().substring(1)).build());
            continue;
        }
        scanParameter(fieldGenericClass);
    }
    return CodeGenPojo.builder().className(classType.getSimpleName()).fields(codeGenFields).request(false).build();
}
 
这里我写了一个工具,用来反射获取参数实际类型的范型class,用于比如BaseResponse<List>这样的参数生成.
public static List<Class> getGenericClassArray(Type genericReturnType){
    List<Class> returnTypes = new ArrayList<>();
    if (genericReturnType instanceof ParameterizedType) {
        Type[] actualTypeArguments = ((ParameterizedType) genericReturnType).getActualTypeArguments();
        Type actualTypeArgument = actualTypeArguments[0];
        returnTypes.add(actualTypeArgument instanceof ParameterizedTypeImpl ? ((ParameterizedTypeImpl) actualTypeArgument).getRawType() : (Class) actualTypeArgument);
        while (actualTypeArgument != null) {
            if (actualTypeArgument instanceof ParameterizedTypeImpl) {
                Type[] actualTypeArgumentsTemp = ((ParameterizedTypeImpl) actualTypeArgument).getActualTypeArguments();
                if (actualTypeArgumentsTemp != null) {
                    actualTypeArgument = actualTypeArgumentsTemp[0];
                    returnTypes.add(actualTypeArgument instanceof ParameterizedTypeImpl ? ((ParameterizedTypeImpl) actualTypeArgument).getRawType() : (Class) actualTypeArgument);
                    continue;
                }
            }
            actualTypeArgument = null;
        }
    }
    returnTypes.add(0,genericReturnType instanceof Class ? (Class) genericReturnType :  ((ParameterizedTypeImpl) genericReturnType).getRawType());
    return returnTypes;
}

public static Class getGenericClass(Type genericReturnType){
    Class returnType = null;
    if (genericReturnType instanceof ParameterizedType) {
        Type[] actualTypeArguments = ((ParameterizedType) genericReturnType).getActualTypeArguments();
        Type actualTypeArgument = actualTypeArguments[0];
        if (actualTypeArgument instanceof ParameterizedTypeImpl) {
            returnType = ((ParameterizedTypeImpl) actualTypeArgument).getRawType();
        }else {
            returnType = (Class) actualTypeArgument;
        }
    }
    return returnType;
}

(6)、文件输出

public void codeGen(){
    log.info("starting code gen ...");
    // 扫描所有实现了@api
    if (!enable) {
        log.info("switch close , code gen end ...");
        return;
    }
    // 扫描注解获取方法
    CodeGen codeGen = scanOpenMethodOrOpenApi();
    if (codeGen == null) {
        log.info("scan @OpenMethod or @OpenApi empty , code gen end ...");
        return;
    }
    Configuration configuration = new Configuration();
    try {
        // step2 获取模版路径
        configuration.setTemplateLoader(new ClassTemplateLoader(getClass(),CODEGEN_FTL_PATH));
        List<CodeGenMethod> methodList = codeGen.getMethodList();
        List<CodeGenPojo> codeGenPojos = Lists.newArrayList();
        methodList.forEach(codeGenMethod -> {
            List<CodeGenPojo> codeGenPojoList = codeGenMethod.getCodeGenPojoList();
            if (codeGenPojoList != null && !codeGenPojoList.isEmpty()) {
                codeGenPojos.addAll(codeGenPojoList);
            }
            codeGenMethod.setParamName(codeGenMethod.getParamName().substring(0,1).toLowerCase()+ codeGenMethod.getParamName().substring(1));
        });
        // 生成dto
        log.info("start parameter code gen ...");
        freeMarkerParameter(codeGenPojos,configuration);
        log.info("end parameter code gen , gen size "+codeGenPojos.size()+" ...");
        // Client
        log.info("start client code gen ...");
        codeGen.setJavaName(codeGen.getClassName()+"Client");
        freeMarkerGen(codeGen,CLIENT_FTL,configuration);
        log.info("end client code gen ...");
        // 生成Proxy
        log.info("start proxy code gen ...");
        codeGen.setJavaName(codeGen.getClassName()+"Proxy");
        freeMarkerGen(codeGen,PROXY_FTL,configuration);
        log.info("end proxy code gen ...");
    } catch (Exception e) {
        e.printStackTrace();
    }
    
    public void freeMarker(Object data, String javaName, String templateName,String path, Configuration configuration){
    Writer out = null;
    try {
        Template template = configuration.getTemplate(templateName);
        String codePath = CodeGenTemplate.class.getClassLoader().getResource("").getPath() + path;
        File codePathFile = new File(codePath);
        if (!codePathFile.exists() && !codePathFile.isDirectory()) {
            codePathFile.mkdirs();
        }
        File docFile = new File( codePath+ "/"+ javaName +".java");
        out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(docFile)));
        template.process(data, out);
    } catch (Exception e) {
        e.printStackTrace();
    }finally {
        try {
            if (null != out) {
                out.flush();
                out.close();
            }
        } catch (Exception e2) {
            e2.printStackTrace();
        }
    }
}
}

二、测试

1、测试入口

        简单写一个runner,来跑下我们的类(由于这里只演示简单代码的生成,所以就不实际去掩饰client端代码调用访问server了,实际上sdk的代码会比这里演示的复杂许多)
 
@Component
@AllArgsConstructor
public class CodeGenRunner implements CommandLineRunner {

    private final CodeGenTemplate codeGenTemplate;
    @Override
    public void run(String... args) throws Exception {
        codeGenTemplate.codeGen();
    }
}

2、Demo Controller

写两个controller的示例,看下生成的对不对
@OpenApi
@RequestMapping("/api/demo")
public class DemoController {

    @PostMapping(value = "/post")
    public BaseResponse<UserVO> queryUser(@RequestBody UserParam arg){
        UserVO userVO = new UserVO();
        userVO.setUsername("Dane.shang");
        userVO.setNickname("快30岁了没去过酒吧");
        return new BaseResponse<>(userVO);
    }

    @PostMapping(value = "/post/list")
    public BaseResponse<List<UserVO>> queryUsers(@RequestBody UserParam arg){
        UserVO userVO = new UserVO();
        userVO.setUsername("Dane.shang");
        userVO.setNickname("快30岁了没去过酒吧");

        UserVO userVOReturn = new UserVO();
        userVOReturn.setUsername(arg.getUsername());

        ArrayList<UserVO> userVOS = Lists.newArrayList(userVO, userVOReturn);
        return new BaseResponse<>(userVOS);
    }
}
这里使用@OpenApi注解,用来扫描整个类的方法生成
 
@RestController
@RequestMapping("/api/demo2")
public class Demo2Controller {

    @OpenMethod
    @PostMapping(value = "/post")
    public BaseResponse<UserVO> queryUser2(@RequestBody UserParam arg){
        UserVO userVO = new UserVO();
        userVO.setUsername("Dane.shang2");
        userVO.setNickname("快30岁了没去过酒吧2");
        return new BaseResponse<>(userVO);
    }

    @PostMapping(value = "/post/list")
    public BaseResponse<List<UserVO>> queryUsers2(@RequestBody UserParam arg){
        UserVO userVO = new UserVO();
        userVO.setUsername("Dane.shang2");
        userVO.setNickname("快三十岁了没去过酒吧2");

        UserVO userVOReturn = new UserVO();
        userVOReturn.setUsername(arg.getUsername());

        ArrayList<UserVO> userVOS = Lists.newArrayList(userVO, userVOReturn);
        return new BaseResponse<>(userVOS);
    }
}
这里使用@OpenMethod注解,用来扫描指定方法
 

3 、测试参数

这里随意写了一些配置, 上面的测试controller 模拟了不同的情况,带范型的,不带范型的等等等,再随意写个包名来测试下..
@Value("${sdk.codegen.enable:false}")
private Boolean enable;
@Value("${sdk.codegen.path}")
private String path;

@Value("${sdk.codegen.package}")
private String packagePath;

@Value("${spring.application.name:Demo}")
private String applicationName;


private static final String SRC = "/src/main/java";
private static final String TARGET = "/target";

private static final String PATTERN_PATH = "com.dane";
private static final String PATTERN_CONTR = "/**/controller/**/*.class";

private static final String CODEGEN_FTL_PATH = "/codegen/";
private static final String CLIENT_FTL = "client.ftl";
private static final String PROXY_FTL = "proxy.ftl";
private static final String PARAM_FTL = "pojo.ftl";

private final static CopyOnWriteArraySet<String> copyOnWriteArraySet = new CopyOnWriteArraySet<>();

4、测试结果

0
可以看到成功运行
 
0
target目录中正常生成了整个代码,把它拷贝进我们的包里
 
0
 
0
 
0
可以看到内容输出正常(这里没有用lombok的原因是提供的sdk要尽量减少依赖)

三、结论

        虽然这里只是简单做了下演示生成代码,实际项目上的代码远比这里要复杂许多,不同的情况条件乃至整个生成的jar包要包含参数加密、返回json序列化、异常判断、请求代理等等等,这里就没做演示了.整个生成代码编写了几天(主要是反射太折磨人了,各种范型,范型中的范型等都要判断生成),用到的技术很简单,但是顺带着也回顾下了反射及java的一些特性.至于打成starter组件供人食用啥的很简单这里就不演示了.
代码生成

关于作者

Dane.shang
快30岁了还没去过酒吧
获得点赞
文章被阅读