前言
最近在给公司做个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文件模版来生成我们的代码.
在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、测试结果
可以看到成功运行
target目录中正常生成了整个代码,把它拷贝进我们的包里
可以看到内容输出正常(这里没有用lombok的原因是提供的sdk要尽量减少依赖)
三、结论
虽然这里只是简单做了下演示生成代码,实际项目上的代码远比这里要复杂许多,不同的情况条件乃至整个生成的jar包要包含参数加密、返回json序列化、异常判断、请求代理等等等,这里就没做演示了.整个生成代码编写了几天(主要是反射太折磨人了,各种范型,范型中的范型等都要判断生成),用到的技术很简单,但是顺带着也回顾下了反射及java的一些特性.至于打成starter组件供人食用啥的很简单这里就不演示了.