需求描述
开发一个扫描类信息(如:方法名,注解名等)的脚本程序,由于扫描的是提供Jar包中的代码,不希望在运行期进行这些逻辑的运行,减少重复的运行操作,希望每次Jar包中代码的变更能够对应一次信息的上报。
我们的项目打包都是通过Maven来进行的,而Maven提供了插件机制,使我们能在Maven管理我们项目的各个生命周期中进行一些骚操作。
我这次的需求刚好用Maven的插件机制满足,在这次需求中学习到了Maven插件的编写,并且踩了很多坑,在这里记录下来,希望能够帮助其他人在开发的时候进行避免。
Maven插件篇
Mojo工程
概念
Mojo 就是 Maven plain Old Java Object。每一个 Mojo 就是 Maven 中的一个执行目标(executable goal),而插件则是对单个或多个相关的 Mojo 做统一分发。一个 Mojo 包含一个简单的 Java 类。插件中多个类似 Mojo 的通用之处可以使用抽象父类来封装。
创建Maven工程
命名:一般来说,我们会将自己的插件命名为 -maven-plugin
,而不推荐使用 maven—plugin
,因为后者是 Maven 团队维护官方插件的保留命名方式,使用这个命名方式会侵犯 Apache Maven 商标。
创建:正常创建Maven项目就可以了,之后我们需要对pom.xml进行一些修改
修改:pom.xml需要添加对maven-plugin-api的依赖,这个依赖里面会包含一些 Mojo 的接口与抽象类。
org.apache.maven
maven-plugin-api
2.0
与普通 pom.xml 文件一个重要的不同之处是它的打包方式:
maven-plugin
编写Maven工程
Mojo 是一个简单的 Java 类,那我们创建第一个 Mojo 类用于打印一行输出。
Mojo类需要继承 AbstractMojo 这个抽象类,并实现了 execute() 方法,该方法就是用来定义这个 Mojo 具体操作内容,我们只需要根据自己的需要来编写自己的实现即可。
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
/**
* @goal hello
*/
public class HelloMojo extends AbstractMojo {
public void execute() throws MojoExecutionException, MojoFailureException {
getLog().info("Hello Mojo");
}
}
@Mojo(name = "hello")
public class HelloMojo extends AbstractMojo {
public void execute() throws MojoExecutionException, MojoFailureException {
getLog().info("Hello Mojo");
}
}
怎么让 Maven 知道这是一个 Mojo 而不是一个普通的 Java 类呢?这里,就需要说一下 Mojo 的查找机制了,在处理源码的时候,plugin-tools 会把使用了 @Mojo 注解或 Javadoc 里包含 @goal 注释的类来当作一个 Mojo 类。
使用 @Mojo 注解,我们需要引入一个新包:
org.apache.maven.plugin-tools
maven-plugin-annotations
3.1
运行自定义Plugin
与使用其它插件类似,我们需要在 pom.xml 文件中引入插件:
XXX
XXX
XXX
我们还可通过配置指定Maven插件在生命周期的哪个阶段执行,还可以通过一些命令赋值。
比如我这次的需求,就需要在编译期执行,将需要扫描的类名传入
com.sankuai
athena-nr-maven-plugin
0.0.2
compile
reporter
XXX
//执行目标
@Mojo(name="reporter" ,requiresDependencyResolution = ResolutionScope.COMPILE)
public class Reporter extends AbstractMojo{
//传入参数
@Parameter(property = "reporter.className")
private String className;
@Override
public void execute() throws MojoExecutionException, MojoFailureException {
//执行逻辑
......
}
}
Maven插件调试
调试方法有很多,选一个比较简单需要使用IDEA的。
在终端使用mvnDebug groupID:artifactID:version:goal
命令来启动插件,这个时候会启动8000端口。
我们还需要使用maven的远程调试,在IDEA中用remote连接过去,并且在插件的execute方法上打上断点。
Maven插件依赖问题(第一个坑)
我们的逻辑是将全类名传入Mojo类,通过全类名去加载该类,然后对该类进行一些操作。
然而却爆出了找不到该类的问题,为什么当前项目的ClassLoader,在运行插件的时候加载不了当前项目的类呢?
在maven reference网站上有关于maven类加载机制的说明。
Maven有四种类加载机制:
- System Classloader
- Core Classloader
- Plugin Classloaders
- Custom Classloaders
我们关心的是第三种,plugin classloaders。这个类加载器从类加载的层次关系来看是继承与System classloader 和Core Classloader的,凭想当然的理解在插件goal执行的时候插件的classloader已经包含的project pom 中申明的依赖包。但是,plugin classloader说明中有这么一句话:
Please note that the plugin classloader does neither contain the dependencies of the current project nor its build output. Instead, plugins can query the project's compile, runtime and test class path from the MavenProject in combination with the mojo annotation requiresDependencyResolution from the Mojo API Specification. For instance, flagging a mojo with @requiresDependencyResolution runtime enables it to query the runtime class path of the current project from which it could create further classloaders.
翻译一下:
请注意,plugin classloader既不包含当前工程的dependencies,也不包含当前工程的输出目录。但是,如果你现在插件运行的时候想引用当前工程的编译(compile)、运行时(runtime)、测试(test)的classpath,可以通过MavenProject 这个组合在成员对象来调用,这个mojo对象需要有“@requiresDependencyResolution”这个annotation
参考该说明,我们知道,maven plugin 不能拿到当前工程的dependencies,也不能包含当前工程的输出目录,我们要向加载我们需要的类可以通过两个方法解决。
解决的第一个方法
在plugin配置的时候为plugin配置节点单独配置一个dependance
com.sankuai
athena-nr-maven-plugin
0.0.2
compile
reporter
XXX
org.apache.httpcomponents
httpclient
4.5.3
但是这样一来,每次升级Jar包,我还要将依赖的配置升一下级,好烦。
解决的第二个方法
动态读取目标项目所依赖的classpath并根据这些classpath生成相应的url数组,以这个url数组作为参数得到的类加载器可以实现在maven插件中动态加载目标项目类及第三方引用包的目的。
@Parameter(defaultValue = "${project}", readonly = true, required = true)
private MavenProject project;
在mojo类中引入MavenProject参数,这个参数是目标项目的抽象,仅仅引入这个参数不需要多余的操作即可。
通过MavenProject参数,我们可以调用project的getCompileClasspathElements()拿到路径的String 列表。
通过这个列表,可以构建URL数组,构建自己的类加载器,该类加载器可以达到实现在maven插件中动态加载目标项目类及第三方引用包的目的。
自定义类加载器篇
首先来复习一下类加载器的一些知识
ClassLoader类加载器
主要的作用是将class文件加载到jvm虚拟机中。jvm启动的时候,并不是一次性加载所有的类,而是根据需要动态去加载类,主要分为隐式加载和显示加载。
隐式加载
程序代码中不通过调用ClassLoader来加载需要的类,而是通过JVM类自动加载需要的类到内存中。例如,当我们在类中继承或者引用某个类的时候,JVM在解析当前这个类的时,发现引用的类不在内存中,那么就会自动将这些类加载到内存中。
显式加载
代码中通过Class.forName()
、this.getClass.getClassLoader.LoadClass()
,自定义类加载器中的findClass()
方法等。
jvm自带的加载器
BootStrap ClassLoader
主要加载%JRE_HOME%\lib
下的rt.jar、resources.jar、charsets.jar和class等。可以通过System.getProperty("sun.boot.class.path")
查看加载路径
Extention ClassLoader
主要加载目录%JRE_HOME%\lib\ext
目录下的jar包和class文件。也可以通过System.out.println(System.getProperty("java.ext.dirs"))
查看加载类文件的路径。
AppClassLoader
主要加载当前应用下的classpath路径下的类。之前我们在环境变量中配置的classpath就是指定AppClassLoader的类加载路径。
类加载器的继承关系
ExtClassLoader,AppClassLoder继承URLClassLoader,而URLClassLoader继承ClassLoader,BoopStrap ClassLoder不在上图中,因为它是由C/C++编写的,它本身是虚拟机的一部分,并不是一个java类。jvm加载的顺序:BoopStrap ClassLoder-〉ExtClassLoader->AppClassLoder
AppClassLoader的父加载器为ExtClassLoader,ExtClassLoader的父加载器为null,BoopStrap ClassLoader为顶级加载器。
demo验证
package test;
public class Test {
public static void main(String []args){
System.out.println(Test.class.getClassLoader().toString());
System.out.println(Test.class.getClassLoader().getParent().toString());
System.out.println(Test.class.getClassLoader().getParent().getParent().toString());
}
}
类加载机制
例如:当jvm要加载Test.class的时候
- 首先会到自定义加载器中查找,看是否已经加载过,如果已经加载过,则返回字节码。
- 如果自定义加载器没有加载过,则询问上一层加载器(即AppClassLoader)是否已经加载过Test.class。
- 如果没有加载过,则询问上一层加载器(ExtClassLoader)是否已经加载过。
- 如果没有加载过,则继续询问上一层加载(BoopStrap ClassLoader)是否已经加载过。
- 如果BoopStrap ClassLoader依然没有加载过,则到自己指定类加载路径下
"sun.boot.class.path"
查看是否有Test.class字节码,有则返回,没有通知下一层加载器ExtClassLoader到自己指定的类加载路径下java.ext.dirs
查看。 - 依次类推,最后到自定义类加载器指定的路径还没有找到Test.class字节码,则抛出异常ClassNotFoundException。
类加载过程
loadClass > findLoadedClass > findClass
protected Class> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,检查是否已经加载过
Class> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//父加载器不为空,调用父加载器的loadClass
c = parent.loadClass(name, false);
} else {
//父加载器为空则,调用Bootstrap Classloader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
//父加载器没有找到,则调用findclass
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
//调用resolveClass()
resolveClass(c);
}
return c;
}
}
自定义类加载器
/**
* created by zhangzhiyuan in 2019/8/2
*/
public class EngineClassLoader extends URLClassLoader {
public EngineClassLoader() {
this(getSystemClassLoader());
}
public EngineClassLoader(ClassLoader parent) {
super(new URL[] {}, parent);
}
public void addURL(URL... urls) {
if (urls != null) {
for (URL url : urls) {
super.addURL(url);
}
}
}
}
加载不到注解问题(第二个坑)
用了该加载器,确实可以通过全类名加载到指定的类,但是又出现了另一个问题。
调用该类是否有某注解的时候,显示没有,并且也无法拿到指定注解
boolean isAnnotationPresent(Class extends Annotation> annotationClass)
但是在打断点的时候,是显示有该注解的,为什么呢?
因为该注解是由我们的自定义类加载器获得的,和我们调用的类虽然是一个类,但不是一个类
一个类由不同的类加载器实例加载的话,会在方法区产生两个不同的类,彼此不可见,并且在堆中生成不同Class实例。
无奈只能全程反射了……麻烦的一匹。
参考
https://blog.csdn.net/u012620150/article/details/78652624
https://blog.csdn.net/m0_37635806/article/details/86711423
https://blog.csdn.net/weixin_40318210/article/details/85055133
https://blog.csdn.net/iteye_10738/article/details/81794471
https://blog.csdn.net/imlsz/article/details/51013556
https://blog.csdn.net/tianlihu/article/details/83669738
https://stackoverflow.com/questions/9318935/get-project-build-directory-from-mavenproject
https://stackoverflow.com/questions/13462107/mavenproject-get-the-available-classes-for-use-on-my-plugin
https://stackoverflow.com/questions/35457401/maven-plugin-api-get-mavenproject-from-artifact