【云+社区年度征文】借鉴了Mybatis源码解决了项目上线时的一个问题
使用了我开发的框架,项目部署时突然出了问题,借鉴了Mybatis源码才解决
一、背景
本篇文章是我对Swagger进行了二次开发,并封装成了一个框架,发布到了maven私服,这样就可以达到拿来即用啦。但是出现了一个问题,导致打包成jar包之后某些功能无法生效,本文特针对这个问题,来阐述如何借鉴了Mybatis源码才解决的。
对swagger扩展的功能,我也对其来龙去脉,做了严谨的分析和相关扩展功能的开发,并记录下来做了梳理,并整理成了一篇文章,在技术创作101训练营第一季的时候,发布到了腾讯云+社区,参加了活动:
这篇名字:《历经14天自定义3个注解解决项目的3个Swagger难题》
链接地址:https://cloud.tencent.com/developer/article/1700641
内容大纲:
这篇文章也收获了很多赞和收藏:
同时也被评为技术创作101训练营-第一季的前10名,拿到了“优秀创作者”的荣誉证书。
在《历经14天自定义3个注解解决项目的3个Swagger难题》中的第三部分:(实战的(二)实战二:减少在Controller中Swagger的代码,使其可以从某些文件中读取信息,自动配置Swagger的功能)。这部分在上线部署的时候突然出现了问题。随后会详细分析一下。
二、分析问题
部署服务之后,发现自己扩展的一个功能尚未生效。
就去docker上扒拉了一下日志。
先看下图,下图是我从docker的启动日志中扒拉出来的。因为我们的服务都部署在了docker中
看一下ReadFromFile.java这个的第298行:
是我读取文件的,初次猜测文件没有读取成功,才会造成的这个问题。
为了验证这个问题是不是服务器上某些配置导致的,我从cmd中使用java -jar命令运行了一下jar包:
还是不行
但是在IDEA中启动时是没有问题的。
整个结构是这样的:
说的有点多,总结一下:
出现的问题是我所开发的这个框架,以一个依赖的形式被其他项目所使用的时候,在IDEA开发环境下运行是可以的。
其中有一个功能是需要读取项目中的某些文件。但是此功能在项目被打成jar包部署在服务器的时候,却出现了问题,无法正常读取文件。
三、解决方案
遇到问题,肯定先百度一下,谷歌一下。
连续搜了好多天,也连续尝试了好多天;
别看下面我差出来了很多,但是几乎也就那几种办法。
我几乎都尝试过,但是都不是我想要的。
我想要的还很特殊:
因为我是开发的一个框架,需要从jar包中读取另一个jar包中的某些文件。
最后不得不放弃百度,就在要快放弃的时候,突然想到Mybatis和我这个是类似的,Mybatis也是一个jar包,Mybatis也是从jar包中读取xxxxMapper.xml文件进行解析的。
Mybatis的这种读取xxxMapper.xml文件的模式和我开发的框架读取md文件的设计居然一模一样。
那只好扒拉一下Mybatis的源代码进行研究一番了;以前想着去一下Mybatis的源码呢,一直没时间。现在正好,趁机学习一下Mybatis源代码。
我从搭建Mybatis的环境开始研究,
也做了比较详细的记录,如果你想学习Mybatis的话,可以持续关注我,我会把学习的过程中记录的一些东西都发布来的。
阅读链接:https://www.yuque.com/docs/share/fa80a044-49be-44ba-aa36-53cca12eee97? 《1、构建源码环境》
解决方案:
- 1、阅读Mybatis源码,找出来Mybatis如何读取的Mapper.xml文件;
- 2、模仿Mybatis读取Mapper.xml文件的流程去改造自己的程序。
四、分析:Mybatis如何加载xxxMapper.xml文件
为了减少篇幅,本篇文章略过如何搭建Mybatis的源码环境,直接描述分析的过程;如果想看,也可以加我微信:weiyi3700,给你初稿版,或者等我整理出来Mybatis源码系列的文章,再来看也是可以的。
https://www.yuque.com/docs/share/fa80a044-49be-44ba-aa36-53cca12eee97? 《1、构建源码环境》
(一)创建一个可以跟踪的程序
为了好跟踪Mybatis源码,使用IDEA创建了2个Model,一个是Mybatis源码项目,一个是测试程序。
如果搭建这套环境可以参考,我写的这篇文章,已经放在语雀上了:
https://www.yuque.com/docs/share/fa80a044-49be-44ba-aa36-53cca12eee97? 《1、构建源码环境》
如果能访问数据,则成功:
(二)逐行分析
1、读取mybatis-config.xml文件
代码如下:
String resource = "mybatis-config.xml";
final Reader reader = Resources.getResourceAsReader(resource);
用鼠标点开getResourceAsReader的时候,就会看到如下图所示的:
可以看到,就是去使用流的形式去读取这个配置文件,并返回一个流对象
在源码中的执行流程如下:
2、创建SqlSessionFactory
SqlSessionFactory的创建的流程太复杂了,我简单总结一下步骤:
(1)从Reader的流中读取Mybatis-config.xml配置文件的数据流;
(2)从流中读取 xml配置文件中的," <configuration> .... </configuration> " 根节点。
(3)从“<configuration> .... </configuration>”根节点中解析每个子节点的数据,例如:“mappers”、“environments”节点等;
(4)解析“mappers”节点,拿到xxxMapper.xml的存放方式和存放路径;
(5)按照“mappers”节点中配置的信息,选择性的进行读取Mapper.xml文件;
(5-1)如果是package方式的话:
(5-1-1)先判断是否为jar包,如果是就以流的形式打开;
(5-1-2)然后会把所有的资源扫描一遍,边扫描边检查是否为我们要寻找的路径,例如:com.truedei.mapper;
(5-1-3)把符合要求的url都会添加到resources中返回。
分析具体流程图
整理出了一个流程图,可以看一下:
到此位置,我们也就知道了,Mybatis是如何扫描到的Mapper.xml文件。
五、总结Mybatis如何扫描到的Mapper.xml文件
我们比较关心的是如何从jar包中扫描到我们想要的资源路径。
由上面的分析可见,是通过ResolverUtil类中的find()方法扫描的。
ResolverUtil.find()又调用VFS类的 VFS.getInstance().list(path)方法进行扫描
在VFS.getInstance().list(path)中比较重要的又两个方法,
一个是:getResources(path)
*(核心)一个是:list(url, path)
在list()接口中进行查找相应的path,并返回一个List<String>集合
而在list()接口中,重要的就是listResources()这个方法了:
在这个方法中,从jar文件流中查找包含path的url;
如果符合要求就添加到List中返回。
protected List<String> listResources(JarInputStream jar, String path) throws IOException {
// Include the leading and trailing slash when matching names
if (!path.startsWith("/")) {
path = "/" + path;
}
if (!path.endsWith("/")) {
path = path + "/";
}
// Iterate over the entries and collect those that begin with the requested path
List<String> resources = new ArrayList<>();
for (JarEntry entry; (entry = jar.getNextJarEntry()) != null;) {
if (!entry.isDirectory()) {
// Add leading slash if it's missing
StringBuilder name = new StringBuilder(entry.getName());
if (name.charAt(0) != '/') {
name.insert(0, '/');
}
// Check file name
if (name.indexOf(path) == 0) {
if (log.isDebugEnabled()) {
log.debug("Found resource: " + name);
}
// Trim leading slash
resources.add(name.substring(1));
}
}
}
return resources;
}
由此可见,最中间,最核心的就是上面这段代码了。
下面name.indexOf(path)就是用来匹配是否开头包含这个路径的
// Check file name
if (name.indexOf(path) == 0) {
if (log.isDebugEnabled()) {
log.debug("Found resource: " + name);
}
// Trim leading slash
resources.add(name.substring(1));
}
那么可以猜测一下,可以修改成我们需要的,例如我想查找包含.md为后缀的文件的路径,那么就可以修改成:
// Check file name
if (name.indexOf(path) > 0) {
if (log.isDebugEnabled()) {
log.debug("Found resource: " + name);
}
// Trim leading slash
resources.add(name.substring(1));
}
可以看到,我们最终想要的,可以模仿的有两个核心的java类:
ResolverUtil.java
VFS.java
这两个类,刚好是Mybatis的i/o模块中的:
二话不说,就拷贝到了我开发的框架的项目中:
六、修改成自己想要的
ResolverUtil中的find方法:
/**
* 查找包下的资源
* @param packageName
* @return
*/
public ResolverUtil find(String packageName) {
//把com.truedei 形式的路径 变成:com/truedei
String path = getPackagePath(packageName);
try {
List<String> children = VFS.getInstance().list(path);
for (String child : children) {
if (child.endsWith(".class")) {
//组装成一个新的文件路径
child = "/md"+child.substring(child.lastIndexOf('/')).replace("class","md");
//child就是我想要加载的md文件的路径
loadJarFileByFile(child);
}
}
} catch (IOException ioe) {
System.out.println("Could not read package: " + packageName+"-->"+ioe);
}
return this;
}
自己写的:
/**
* 加载jar包的资源文件
* @param file
* @return
*/
private void loadJarFileByFile(String file) {
InputStream stream = this.getClass().getResourceAsStream(file);
if(stream==null){
return ;
}
BufferedReader br = null ;
try {
br = new BufferedReader(new InputStreamReader(stream,"UTF-8")) ;
String s=null ;
while((s=br.readLine()) !=null){
//把每一行数据都添加到全局的List中后期对其方便处理
fileContentList.add(s);
}
br.close();
} catch (FileNotFoundException e) {
System.out.println("FileNotFoundException:"+e);
} catch (IOException e) {
System.out.println("IOException :"+e);
}finally {
if(br !=null){
try {
br.close();
} catch (IOException e) {
System.out.println("close br error:"+e);
}
}
}
}
七、成功的喜悦
可以看到我想解析的md文件的数据也都被解析出来了: