造一個方形輪子文章目錄:造一個方形的輪子
01艾少、解決遺留問題
上一篇的最后說要把BeansInitUtil類代碼優(yōu)化一下,先來弄一下這個吧伦籍,順便把加入AOP導(dǎo)致多遍歷了一遍目錄的問題解決一下爆安。
調(diào)整的有點多醒串,就不放代碼了,寫一下主要改動的文件及改動內(nèi)容吧:
com.jisuye.core包:
BeansMap.java 全方法靜態(tài)化調(diào)用靜態(tài)的個各map容器
BeanObject.java 添加了BeanClass記錄bean類型宠叼,在加載AOP切面時使用
ControllerObject.java 添加了beanKey字段先巴,將獲取Bean從初始化階段放到調(diào)用階段,這么做是為了去掉添加AOP功能后多的一遍目錄遍歷
ControllerObject.java 部分代碼:
/**
* 反射執(zhí)行controller方法
* @param req
* @return
*/
public Object invoke(HttpServletRequest req){
//...
try {
// 延時從容器中獲取對象
if(object == null){
this.setObject(BeansMap.get(beanKey).getObject());
}
Object o = this.getMethod().invoke(this.getObject(), os);
return o;
} catch (Exception e) {
log.error("Controller method.invoke() is error!", e);
throw new SquareException("Controller method.invoke() is error!", e);
}
}
com.jisuye.util包:
BeansInitUtil.java 調(diào)整順序把AOP放到IOC初始化之后DI初始化之前冒冬,減少了一次目錄遍歷
BeansInitUtil.java 放部分代碼:
public static void init(Class clazz){
String path = clazz.getResource("").getPath();
log.info("===bean init path:{}", path);
File root = new File(path);
// 處理控制反轉(zhuǎn)(加載aop切面,controller)
initFile(root);
// 處理aop類
initAop();
// 處理依賴注入
initDI();
}
private static void initAop(){
List<BeanObject> list = new ArrayList<>();
// 循環(huán)所有Bean處理Aop
for(Map.Entry entry : BeansMap.entrySet()) {
BeanObject beanObject = (BeanObject) entry.getValue();
// 如果已經(jīng)處理過伸蚯,則跳過
if (beanObject.getObject() != null) {
break;
}
beanObject.setObject(getInstance(beanObject.getBeanClass(), beanObject.getSrcObj()));
}
}
// ...
private static void loadClass(File file){
// ...
if(annotation instanceof Service){
tmp_name = ((Service)annotation).value();
} else if(annotation instanceof Component) {
tmp_name = ((Component)annotation).value();
} else if(annotation instanceof Controller) {
initController(clzz, ((Controller)annotation).value());
} else if(annotation instanceof Aspect){
// 添加加載AOP切面
loadAop(clzz);
}
// ...
}
02、編譯打包問題整理
在真正開始寫編譯打包功能之前简烤,我一直覺得這應(yīng)該是一個比較簡單的功能剂邮,實現(xiàn)應(yīng)該也沒什么難度,事實是我的臉現(xiàn)在依然很疼横侦。挥萌。。
起初我想使用maven自帶的打包插件枉侧,打一個jar包出來引瀑,看了一下要指定主方法所在類,看起來有點麻煩榨馁,而且打出來看包是不包含依賴的憨栽,不可以直接運行,當然有其它的插件能夠?qū)崿F(xiàn)把依賴jar包打包進去的功能翼虫,但還要是配置主類屑柔,于是我想自己定一個插件吧(這大概是打臉的開始)。
只實現(xiàn)自定義插件很容易蛙讥,但要實現(xiàn)我的功能锯蛀,第一個功能就是指定Main-Class 看了一下灭衷,編譯完的文件里并沒有MANIFEST.MF清單文件次慢,只有最終打出來的jar包里才有,于是第一個問題就是:
如何向jar包里的文件寫入內(nèi)容?
看了spring-boot-maven-plugin 的源碼迫像,發(fā)現(xiàn)他是在package完成后跟著執(zhí)行了自己的repackage 流程劈愚,將package打出來的jar包,復(fù)制了一份闻妓。于是我也按這個思路實現(xiàn)了一下菌羽,在復(fù)制的過程中判斷是不是MANIFEST.MF 如果是的話向文件尾追加Main-Class配置 ,Main-Class指定的main方法所在類是通過遍歷目錄文件找到帶有@SquareApplication 注解的類獲得的由缆。Main-Class的問題解決了注祖,再來看一下沒有打包依賴的問題。
如何將依賴的jar打包到一起均唉?
有了上邊的經(jīng)驗是晨,這次和想直接把依賴的jar找出來,在復(fù)制的過程中保存進去這樣應(yīng)該就可以了舔箭,事實是我做到了罩缴,保存到了jar包的lib目錄下,結(jié)果依賴還是找不到层扶,后來查了一下箫章,在jar內(nèi)部的文件,引用的時候都是通過***.jar!/a/b/c 這樣的方式標記的镜会,Java中自帶的ClassLoader只能接受一個“!”的這種路徑檬寂,也就是可以加載jar包里的類,但對jar包里的jar包稚叹,也就是路徑上要帶兩個“!” 這種就無能為力了焰薄,SpringBoot 是自己實現(xiàn)了一個ClassLoader,重寫加載路徑方法扒袖,搞定的塞茅,簡單實現(xiàn)的話我參考了一下maven-assembly-plugin 他是將依賴jar包里的內(nèi)容全部解壓到當前jar里,這樣就不會出來找不到依賴的類的問題(依然簡單粗暴)季率。
編譯打包的問題解決了野瘦,打出了可以執(zhí)行的jar包,然后就發(fā)現(xiàn)我的程序?qū)崿F(xiàn)有很大的問題飒泻,之前大量使用的File加載反射類鞭光,是沒有問題的,但是放到Jar包中就不行了泞遗,因為File不能直接使用***.jar!/a/b
這種形式的路徑惰许。也就不能使用File進行目錄遍歷。
如何在Jar包中遍歷路徑史辙?
這個問題我能想到的辦法就是在程序中特殊處理了汹买,判斷一下當前是在jar包中還是在程序中佩伤,如果是在jar包中,則加載JarFile 獲取Jar包中的條目晦毙,遍歷是否需要加載就可以了生巡。
03、編譯打包插件
這里先看一下核心的代碼见妒,全部代碼文章最后會給出下載地址孤荣。
BuildMojo.java:
package com.jisuye;
// import ...
@Mojo(name = "repackage", defaultPhase = LifecyclePhase.PACKAGE, requiresProject = true,
requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME,
requiresDependencyCollection = ResolutionScope.COMPILE_PLUS_RUNTIME)
public class BuildMojo extends AbstractMojo {
@Parameter(defaultValue = "${project}", readonly = true, required = true)
private MavenProject project;
@Component
private MavenProjectHelper projectHelper;
@Parameter(defaultValue = "${project.build.directory}", required = true)
private File outputDirectory;
@Parameter(defaultValue = "${project.build.finalName}", readonly = true)
private String finalName;
public void execute() throws MojoExecutionException, MojoFailureException {
getLog().info("square maven plugin exe...");
getLog().info("outputDir:"+outputDirectory.getPath());
Artifact source = project.getArtifact();
try {
// 取package打出的jar包
File file = source.getFile();
getLog().info("file name name ======"+file.getName());
JarFile jarFile = new JarFile(file);
// 創(chuàng)建臨時jar包
File tmpFile = new File(outputDirectory.getPath()+"/temp.jar");
JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(tmpFile));
List<String> fileList = new ArrayList<String>();
Enumeration<?> jarEntries = jarFile.entries();
// 循環(huán)復(fù)制文件
while (jarEntries.hasMoreElements()) {
JarEntry entry = (JarEntry) jarEntries.nextElement();
// getLog().info("filename:"+entry.getName());
fileList.add(entry.getName());
InputStream entryInputStream = jarFile.getInputStream(entry);
// 如果是清單文件,則追加主類
if(entry.getName().endsWith("MANIFEST.MF")){
BufferedReader br = new BufferedReader(new InputStreamReader(entryInputStream));
String line;
StringBuilder sb = new StringBuilder();
while ((line = br.readLine()) != null) {
if(!line.equals("")) {
sb.append(line).append("\n");
}
}
String mainClass = findMainClass();
getLog().info("mainclass:"+mainClass);
if(mainClass.equals("")){
getLog().error("No found Main-Class!! Plase use the @SquareApplication settings");
} else {
sb.append("Main-Class: ").append(mainClass).append("\n");
}
jarOutputStream.putNextEntry(new JarEntry(entry.getName()));
jarOutputStream.write(sb.toString().getBytes());
} else {
jarOutputStream.putNextEntry(entry);
byte[] buffer = new byte[1024];
int bytesRead = 0;
while ((bytesRead = entryInputStream.read(buffer)) != -1) {
jarOutputStream.write(buffer, 0, bytesRead);
}
}
}
addDependenceJar(project.getArtifacts(), jarOutputStream, fileList);
jarOutputStream.close();
updateJar(file, tmpFile);
} catch (IOException e) {
getLog().error("load jar file error!", e);
}
}
/**
* 更新jar文件须揣,將原文件保存為**.jar.old
* @param oldFile 原文件
* @param newFile 添加清單及依賴后的完整包
* @throws IOException
*/
private void updateJar(File oldFile, File newFile) throws IOException {
String fileName = oldFile.getName();
File backup = new File(outputDirectory.getPath()+"/"+fileName+".old");
FileOutputStream fos = new FileOutputStream(backup);
FileInputStream fis = new FileInputStream(oldFile);
byte[] buffer = new byte[1024];
int bytesRead = 0;
while ((bytesRead = fis.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
}
fis.close();
fos.close();
oldFile.delete();
if(finalName != null && !finalName.equals("")){
fileName = finalName+".jar";
}
File newJar = new File(outputDirectory.getPath()+"/"+fileName);
fos = new FileOutputStream(newJar);
fis = new FileInputStream(newFile);
buffer = new byte[1024];
while ((bytesRead = fis.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
}
fis.close();
fos.close();
newFile.delete();
}
/**
* 添加依賴的jar
* @param artifacts 依賴Jar列表
* @param jarOutputStream 當前repackage包輸出流
* @param fileList 已有文件列表(防止重復(fù))
* @throws IOException
*/
private void addDependenceJar(Set<Artifact> artifacts, JarOutputStream jarOutputStream, List<String> fileList) throws IOException {
// save dependence jar
for (Artifact artifact : artifacts) {
JarFile jarFile = new JarFile(artifact.getFile());
Enumeration<?> jarEntries = jarFile.entries();
while (jarEntries.hasMoreElements()) {
JarEntry entry = (JarEntry) jarEntries.nextElement();
if(fileList.contains(entry.getName())){
// already added skipping
// getLog().info(entry.getName()+" already added, skipping");
continue;
}
fileList.add(entry.getName());
InputStream entryInputStream = jarFile.getInputStream(entry);
jarOutputStream.putNextEntry(entry);
byte[] buffer = new byte[1024];
int bytesRead = 0;
while ((bytesRead = entryInputStream.read(buffer)) != -1) {
jarOutputStream.write(buffer, 0, bytesRead);
}
}
}
}
/**
* 遍歷目錄查找主類
* @return
*/
private String findMainClass(){
Build build = project.getBuild();
getLog().info("build source dir:"+build.getSourceDirectory());
File f = new File(build.getSourceDirectory());
StringBuilder mainClass = new StringBuilder();
getMainClass(f, mainClass);
return mainClass.toString();
}
/**
* 查找還@SquareApplication注解的類
* @param file 文件
* @param mainClass 要返回的主類
*/
private void getMainClass(File file, StringBuilder mainClass){
if(!mainClass.toString().equals("")){
return;
}
File[] fs = file.listFiles();
for (File f : fs) {
if(f.isDirectory()){
// 遞歸目錄
getMainClass(f, mainClass);
} else {
// 處理class
try {
if(!f.getName().endsWith(".java")){
return;
}
BufferedReader br = new BufferedReader(new FileReader(f));
String line, packageStr = "";
while((line = br.readLine()) != null){
line = line.trim();
if(line.startsWith("package ")){
packageStr = line.substring(8).replace(";", "");
}
if(line.equals("@SquareApplication")){
mainClass.append(packageStr+"."+f.getName().replace(".java", ""));
}
}
} catch (Exception e) {
getLog().error("Find Main-Class error!", e);
}
}
}
}
}
代碼中有注釋盐股,基本就是按前邊說的思路實現(xiàn)了一下。這塊除了對maven插件不太熟悉耻卡,別的都還好遂庄,多看看前輩們寫的插件,習(xí)慣就好了劲赠。
04涛目、Square框架修改
如上文所說,打包這后凛澎,原來的代碼問題很大霹肝,改了一些地方。
ClassesPathUtil.java:
//...
public ClassesPathUtil(Class clzz){
String basePath = clzz.getResource("").getPath();
log.info("basePath+++++{}", basePath);
// ..../classes
if(basePath.indexOf("classes")>0) {
projectPath = basePath.substring(0, basePath.indexOf("classes") + 7);
} else {
projectPath = basePath.substring(0, basePath.indexOf("!")+1);
}
publicPath = setPublic(projectPath, "/public");
}
//...
這里要加上判斷是在jar包中的情況塑煎,要特殊處理沫换。
LoadApplicationYmlUtil.java:
// ...
public static Map<String, Object> load(String projectPath){
Map<String, Object> retMap = new HashMap<>();
try {
InputStream is;
if(projectPath != null && projectPath.indexOf("!") > 0){
is = ClassLoader.getSystemResourceAsStream("application.yml");
} else {
projectPath += "/application.yml";
log.info("load yml file path:{}", projectPath);
is = new FileInputStream(projectPath);
}
Yaml yaml = new Yaml();
Map<String, Object> map = (Map<String, Object>)yaml.load(is);
if(map != null && map.size()>0){
for(Map.Entry e : map.entrySet()) {
convert("", retMap, e);
}
}
} catch (FileNotFoundException e) {
log.error("load application.yml file error.", e);
}
return retMap;
}
// ...
這里也要判斷如果是在jar包里就不能使用File的方式獲取文件流,可以通過ClassLoader.getSystemResourceAsStream()方法獲得最铁。
BeanInitUtil.java:
// ...
public static void init(Class clazz){
String path = clazz.getResource("").getPath();
log.info("===bean init path:{}", path);
if(path.indexOf("!")<0) {
File root = new File(path);
// 處理控制反轉(zhuǎn)(加載aop切面,controller)
initFile(root);
} else {
// 處理jar包內(nèi)的反射邏輯
initJar(path);
}
// 處理aop類
initAop();
// 處理依賴注入
initDI();
}
private static void initJar(String jarPath){
try {
String packageStr = jarPath.substring(jarPath.indexOf("!")+2).replaceAll("/", ".");
log.info("packageStr :{}", packageStr);
jarPath = jarPath.substring(0, jarPath.indexOf("!")).replace("file:/", "");
log.info("jar file path:{}", jarPath);
JarFile jarFile = new JarFile(new File(jarPath));
// 獲取jar文件條目
Enumeration<JarEntry> enumFiles = jarFile.entries();
JarEntry entry;
while(enumFiles.hasMoreElements()){
entry = enumFiles.nextElement();
String className = entry.getName().replaceAll("/", ".");
// 只處理自己包下的class文件
if(className.startsWith(packageStr) && className.indexOf(".class")>=0){
className = className.substring(0,className.length()-6).replace("/", ".");
log.info("class:{}", className);
loadClass(className);
}
}
} catch (IOException e) {
log.error("load jar file error!", e);
}
}
添加了initJar方法讯赏,從jar包中遍歷初始化Bean。
其它的一些聯(lián)動修改冷尉,可以在源碼中查看漱挎。
05、測試項目
新創(chuàng)建一個square-demo項目雀哨,pom.xml文件中引入我的們框架及打包工具(需要先在框架及打包工具目錄下執(zhí)行mvn clean install)
<dependencies>
<dependency>
<groupId>com.jisuye</groupId>
<artifactId>square</artifactId>
<version>0.1-SNAPSHOT</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>com.jisuye</groupId>
<artifactId>square-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
創(chuàng)建一個測試的HelloController.java:
package com.jisuye.controller;
// import ...
@Controller("/")
public class HelloController {
@GetMapping("/hello")
public String hello(@RequestParam("name") String name){
return "hello "+name;
}
}
啟動程序T.java:
package com.jisuye;
// import ...
@SquareApplication
public class T {
public static void main(String args[]){
run(T.class, args);
}
}
添加application.yml:
server:
port: 8765
servlet:
context-path: /square-demo
好的磕谅,現(xiàn)在我們試一下開發(fā)中啟動程序,支持T.main()方法雾棺,查看控制臺輸出:
18:58:04.793 [main] INFO com.jisuye.core.SquareApplication -
__________ ____
| ____ |/ ___| __ _ _ _ __ _ _ __ ___
| | | |\___ \ / _` | | | |/ _` | '__/ _ \
| |____| | ___) | (_| | |_| | (_| | | | __/
|__________||____/ \__, |\__,_|\__,_|_| \___|
======================|_|====================
:: Square :: (v0.1)
18:58:04.794 [main] INFO com.jisuye.util.BeansInitUtil - ===bean init path:/C:/Users/admin/idea/square-demo/target/classes/com/jisuye/
18:58:04.795 [main] INFO com.jisuye.util.BeansInitUtil - load bean class:com.jisuye.controller.HelloController
18:58:04.805 [main] INFO com.jisuye.util.BeansInitUtil - classPath:, methods.length:10
18:58:04.813 [main] INFO com.jisuye.util.BeansInitUtil - add controller key:get:/hello
18:58:04.813 [main] INFO com.jisuye.util.BeansInitUtil - load bean class:com.jisuye.T
18:58:04.813 [main] INFO com.jisuye.core.SquareApplication - beans size is:3
....
18:58:06.911 [main] INFO com.jisuye.core.SquareApplication - Tomcat started on port(s): 8765 with context path '/square-demo'
18:58:06.911 [main] INFO com.jisuye.core.SquareApplication - Started Application in 2233 ms.
程序啟動成功膊夹,在瀏覽器訪問:http://localhost:8765/square-demo/hello?name=ixx
返回:hello ixx
到目前為止都沒有問題,然后我們來試一下打包捌浩,在square-demo項目目錄下執(zhí)行mvn clean package
放刨,查看輸出:
....
[INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ square-demo ---
[INFO] Building jar: C:\Users\admin\idea\square-demo\target\square-demo-1.0-SNAPSHOT.jar
[INFO]
[INFO] --- square-maven-plugin:0.1-SNAPSHOT:repackage (default) @ square-demo ---
[INFO] square maven plugin exe...
[INFO] outputDir:C:\Users\admin\idea\square-demo\target
[INFO] file name name ======square-demo-1.0-SNAPSHOT.jar
[INFO] build source dir:C:\Users\admin\idea\square-demo\src\main\java
[INFO] mainclass:com.jisuye.T
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 3.737 s
[INFO] Finished at: 2019-08-14T19:00:23+08:00
[INFO] ------------------------------------------------------------------------
build success! 我們來看一下target目錄:
C:\Users\admin\idea\square-demo>ls target
classes maven-archiver maven-status square-demo-1.0-SNAPSHOT.jar square-demo-1.0-SNAPSHOT.jar.old
有一個square-demo-1.0-SNAPSHOT.jar
和一個square-demo-1.0-SNAPSHOT.jar.old
,跟我們插件實現(xiàn)效果一樣尸饺,.old是package打出來的而.jar是我們repackage過的进统,我們來啟動看一下拓诸,執(zhí)行java -jar target/square-demo-1.0-SNAPSHOT.jar
.....
19:04:17.672 [main] INFO com.jisuye.core.SquareApplication - Tomcat started on port(s): 8765 with context path '/square-demo'
19:04:17.673 [main] INFO com.jisuye.core.SquareApplication - Started Application in 944 ms.
可以看到也啟動成功了,現(xiàn)在訪問一下
在瀏覽器訪問:http://localhost:8765/square-demo/hello?name=ixx
返回:hello ixx
06麻昼、遺留問題
編譯打包基本完成了,但還有很多問題:
1馋辈、沒有實現(xiàn)靜態(tài)文件目錄抚芦,public 的復(fù)制
2、沒有處Square理框架的依賴關(guān)系
3迈螟、現(xiàn)有方式加載Bean有可能加載多余的類
問題留給下一篇....
這一篇寫出來感覺沒多少東西但在做的過程中確實是出現(xiàn)太多跟預(yù)想的不一樣的結(jié)果叉抡,往往是為了解決一個問題,中間要走錯很多路答毫,找時間得好好看看Maven的實現(xiàn)原理褥民。
本篇代碼地址: https://github.com/iuv/square/tree/square9
打包插件地址: https://github.com/iuv/square-maven-plugin
演示項目地址: https://github.com/iuv/square-demo
本文作者: ixx
本文鏈接: http://jianpage.com/2019/08/15/square9
版權(quán)聲明: 本作品采用 知識共享署名-非商業(yè)性使用-相同方式共享 4.0 國際許可協(xié)議 進行許可。轉(zhuǎn)載請注明出處洗搂!