在上一篇博客中我们讲解了简单的自定义插件开发工作,今天我们继续讲解一下自定义插件开发中可能涉及到的其他内容。比如传参、自定义类加载、扩展maven-compiler-plugin的processor等功能。
在前面一篇博客中我们简单入门了解了Maven插件开发的相关流程,具体操作是读取文件然后显示出来。如果忘了,可以先去复习一下——自定义Maven插件开发(一)。
在实际应用中我们可以通过maven插件结合模板引擎来生成代码。当然我们也可以通过maven插件来生成一些额外的代码,比如,像querydsl这种来生成带Q的实体类对象等。
还是直接实战更快,比如这里我们通过使用FreeMarker在指定的目录下生成基本的entity实体对象。
一、FreeMarker
既然使用FreeMarker来实际操作,那么在使用之前,我们就来介绍一下什么是FreeMarker。
在官方文档介绍中说FreeMarker是一款模板引擎: 即一种基于模板和要改变的数据, 并用来生成输出文本(HTML网页,电子邮件,配置文件,源代码等)的通用工具。 它不是面向最终用户的,而是一个Java类库,是一款程序员可以嵌入他们所开发产品的组件。
也就是类似于Python中的Jinja2,PHP中的Smarty这类工具。
模板编写为FreeMarker Template Language (FTL)。它是简单的,专用的语言, 不是像PHP那样成熟的编程语言。 那就意味着要准备数据在真实编程语言中来显示,比如数据库查询和业务运算, 之后模板显示已经准备好的数据。在模板中,你可以专注于如何展现数据, 而在模板之外可以专注于要展示什么数据。
因为我们的重点内容不是FreeMarker,所以关于更多的内容与语法可以参考官网。
二、实战
接下来我们还是直接实战,新建code-maven-plugin项目,类似第一节这里就不多说了。因为我们需要通过FreeMarker去生成模板代码,所以在resources下新建entity.ftl文件用于代码模板生成。
这里entity.ftl的文件很简单,就是一个简单的Entity类,set/get方法则是通过lombok生成。代码如下:
package ${config.pkg}.${config.entityPkg};
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = false)
public class ${entityName} {
<#list params as param>
private ${param.fieldType} ${param.fieldName};
</#list>
}
这里我们就是简单使用了一下FreeMarker的基本自定义参数,当然FreeMarker的用法也非常多,还可以实现自定义函数等多种用法,可以具体查看文档。
接着我们编写CodeMojo代码,其实和上一章差不多,只是这里我们将FreeMarker结合起来了。代码如下:
package net.anumbrella;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import org.apache.commons.io.FileUtils;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import java.io.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static freemarker.template.Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS;
@Mojo(name = "code", defaultPhase = LifecyclePhase.GENERATE_SOURCES)
public class CodeMojo extends AbstractMojo {
private static final Configuration FREE_MARKER_CONFIGURATION = new Configuration(DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
private String basePkgDir;
@Parameter(defaultValue = "${project}", required = true, readonly = true)
MavenProject project;
@Parameter
private Config config;
public void execute() throws MojoExecutionException, MojoFailureException {
initParams();
generatorCode();
}
private void initParams() {
basePkgDir = project.getBuild().getSourceDirectory() + File.separator + (config.getPkg() + ".").replaceAll("\\.", "/");
}
private void generatorCode() throws MojoFailureException {
try {
String name = "entity";
InputStream source = getTemplate(name);
StringWriter writer = new StringWriter();
File targetFile = new File(basePkgDir + File.separator + config.getEntityPkg().toLowerCase(), "Person.java");
Template template = new Template(name, new InputStreamReader(source, "utf-8"), FREE_MARKER_CONFIGURATION);
Map<String, Object> data = new HashMap<String, Object>();
data.put("entityName", "Person");
List<Map<String, String>> paramsList = new ArrayList<Map<String, String>>();
Map<String, String> IdMap = new HashMap<String, String>();
IdMap.put("fieldType", "Integer");
IdMap.put("fieldName", "id");
paramsList.add(IdMap);
Map<String, String> nameMap = new HashMap<String, String>();
nameMap.put("fieldType", "String");
nameMap.put("fieldName", "username");
paramsList.add(nameMap);
Map<String, String> passwordMap = new HashMap<String, String>();
passwordMap.put("fieldType", "String");
passwordMap.put("fieldName", "password");
paramsList.add(passwordMap);
data.put("params", paramsList);
data.put("config", config);
template.process(data, writer);
String content = writer.getBuffer().toString();
FileUtils.writeStringToFile(targetFile, content, "utf-8", false);
} catch (IOException e) {
e.printStackTrace();
} catch (TemplateException e) {
e.printStackTrace();
}
}
private InputStream getTemplate(String type) {
String name = type + ".ftl";
return this.getClass().getResourceAsStream("/" + name);
}
}
其中@Parameter这个是在插件代码中使用的参数,这个是可以使用Maven中内置参数的,也就是不需要我们手动传入的。我们知道
Maven共有6类属性:
内置属性(Maven预定义,用户可以直接使用)
-
${basedir}
表示项目根目录,即包含pom.xml文件的目录; -
${version}
表示项目版本; -
${project.basedir}
同${basedir}
; -
${project.baseUri}
表示项目文件地址; -
${maven.build.timestamp}
表示项目构件开始时间; -
${maven.build.timestamp.format}
表示属性${maven.build.timestamp}
的展示格式,默认值为yyyyMMdd-HHmm;
POM属性(使用pom属性可以引用到pom.xml文件对应元素的值)
-
${project.build.directory}
表示主源码路径; -
${project.build.sourceEncoding}
表示主源码的编码格式; -
${project.build.sourceDirectory}
表示主源码路径; -
${project.build.finalName}
表示输出文件名称; -
${project.version}
表示项目版本,与${version}
相同;
比如@Parameter(property = "project.basedir", readonly = true)
,就是表示项目的具体地址,更多其他Maven属性可以查看Maven属性文档,其他灵活用法也可以参考官方的示例。
当然plugin中还有很多其他的注解,关于插件中其他注解信息可以查看文档。
如下面我们传入一个自定义的Config对象,Cofig类如下:
package net.anumbrella;
/**
* @auther anumbrella
*/
public class Config {
private String pkg;
/**
* entity的包名
*/
private String entityPkg = "entity";
public String getPkg() {
return pkg;
}
public void setPkg(String pkg) {
this.pkg = pkg;
}
public String getEntityPkg() {
return entityPkg;
}
public void setEntityPkg(String entityPkg) {
this.entityPkg = entityPkg;
}
}
@Parameter
private Config config;
在插件里面,我们有一个通过对象传参的情况,这种情况在pom.xml里面,需要如下配置即可。
<config>
<pkg>test</pkg>
<entityPkg>model</entityPkg>
</config>
同样的,我们执行mvn clean install
,然后再具体项目中引入plugin。
如下,我在另一个项目中使用自定义的插件,在pom.xml中引入。
<plugin>
<groupId>net.anumbrella</groupId>
<artifactId>code-maven-plugin</artifactId>
<version>1.0-SNAPSHOT</version>
<configuration>
<config>
<pkg>net.anumbrella.oauth2</pkg>
</config>
</configuration>
</plugin>
因为我的项目包命名为net.anumbrella.oauth2,所以这里传入的参数为具体包名。制定好plugin配置信息,然后执行mvn code:code
可以发现在entity目录下生成了我们预先配置好的Person.java文件。
这里只是简单生成了一个代码,比如我们可以扩展根据连接数据库查询到相关表,然后表的字段映射处理,就可以生成需要的Entity类了。同时对于基础框架,也可以定义好项目框架结构直接生成相关代码,比如service层,rest层,dao层等。
三、扩展
1、自定义ClassLoader
比如我们这里像要实现querydsl这种在每个entity类中,去扫描获取到带有注解@Entity的类,然后生成带Q的entity。这个要如何实现,也许你会说我们能到获取到项目地址,然后扫描类根据拿到的当前类去操作,比如Class.getAnnotations()这种方法。
比如,还有我们想获取多个继承最顶层的泛型的类型,一般我们会使用
ParameterizedType parameterizedType = (ParameterizedType) temp.getGenericSuperclass();
Type[] actualType = parameterizedType.getActualTypeArguments();
去获取父类泛型定义。但是很遗憾maven插件在执行时,所有类并没有加载到JVM里面,所以你没法通过class去操作一些列想要的办法。
那怎么办?这里我们使用URLClassLoader,因为我们能够获取到目录下的文件,所以我们手动加载到JVM里面去。
我们在plugin里面初始化变量时,初始化ClassLoader。
@Parameter(defaultValue = "${project.compileClasspathElements}", readonly = true, required = true)
private List<String> compilePath;
@Parameter(defaultValue = "${plugin}", readonly = true)
PluginDescriptor pluginDescriptor;
// 初始化classloader
List<URL> pathUrls = new ArrayList<>();
for (String mavenCompilePath : project.getCompileClasspathElements()) {
pathUrls.add(new File(mavenCompilePath).toURI().toURL());
}
URL[] urlsForClassLoader = pathUrls.toArray(new URL[pathUrls.size()]);
classloader = new URLClassLoader(urlsForClassLoader, Thread.currentThread().getContextClassLoader());
同时需要加入上面两个@Parameter,但是有时候可能依赖不能完全加载,所以还得更改Mojo如下:
@Mojo(name = "code", defaultPhase = LifecyclePhase.GENERATE_SOURCES, threadSafe = true,
requiresDependencyResolution = ResolutionScope.COMPILE
主要是为了添加requiresDependencyResolution来解决依赖不能加载情况。
2、将插件绑定在生命周期
每次我们如果要执行生成代码,除了在命令中执行mvn code:code
外,我们还可以将plugin绑定到maven的生命执行周期中去。比如:
<plugin>
<groupId>net.anumbrella</groupId>
<artifactId>code-maven-plugin</artifactId>
<version>1.0-SNAPSHOT</version>
<configuration>
<config>
<pkg>net.anumbrella.oauth2</pkg>
</config>
</configuration>
<executions>
<execution>
<id>code</id>
<goals>
<goal>code</goal>
</goals>
<phase>compile</phase>
</execution>
</executions>
</plugin>
这里我们将code代码生成绑定在maven的compile生命周期中,所以每次执行mvn compile
就都会执行plugin命令操作。
由于篇幅过长,扩展maven-compiler-plugin的processor的功能将在下章节讲解。
代码实例:Chapter2
参考
- http://maven.apache.org/plugin-developers/index.html
- http://freemarker.foofun.cn/index.html