Springboot Fat-jar拆分
解决的问题
springboot自带的打包插件将整个工程打包为一整个jar,所有的源码、依赖jar、资源配置文件都在一个jar中,虽然方便,但是生产中也有许多问题。
传输慢
springboot一般运行在linux环境,当需要查看jar中文件内容时,需要下载到本地windows环境,但是jar过大时,下载很费时,如果有网络限制,可能会下载失败。
查看内容不方便
由于是所有文件都在jar包中,查看时需要解压才能看到,比如springboot配置文件和日志配置文件
修改难度大
当我们想要修改配置或者增删依赖jar时,fat-jar就不是很方便,需要解压整个jar,修改文件后再压缩成jar。
升级包过大
实际项目中,当客户使用了我们的springboot开发的产品后,如果后续需要升级,就要做一个升级包,因此升级包要包含整个fat-jar,可能只改了很少的代码,但是仍然需要全量替换。
解决方式
拆分fat-jar
1.将依赖jar从fat-jar中移动到外部的lib文件夹中
2.将配置文件从fat-jar中移动到外部的resources文件夹中
3.源码还是放置到fat-jar中
4.将lib中的依赖jar和resources加入到classpath中
以上操作通过maven可以全部完成,只需修改pom的bulid标签内容,影响很小。
bulid内容如下:
<properties>
<output.dependence.file.path>lib/</output.dependence.file.path>
<output.resource.file.path>resources/</output.resource.file.path>
</properties>
<build>
<finalName>springboot-server</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<!-- 剔除配置文件 -->
<excludes>
<exclude>*.properties</exclude>
<exclude>*.xml</exclude>
<exclude>algorithm/**</exclude>
<exclude>static/**</exclude>
</excludes>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<!-- MANIFEST.MF 中 Class-Path 各个依赖加入前缀 -->
<!--lib文件夹内容,需要 maven-dependency-plugin插件补充-->
<classpathPrefix>${output.dependence.file.path}</classpathPrefix>
<!-- jar包不包含唯一版本标识 -->
<useUniqueVersions>false</useUniqueVersions>
<!--指定入口类 -->
<mainClass>com.xxx.xxxApplication</mainClass>
</manifest>
<manifestEntries>
<!--MANIFEST.MF 中 Class-Path 加入自定义路径,多个路径用空格隔开 -->
<!--此处resources文件夹的内容,需要maven-resources-plugin插件补充上-->
<Class-Path>./${output.resource.file.path}</Class-Path>
</manifestEntries>
</archive>
<outputDirectory>${project.build.directory}</outputDirectory>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/${output.dependence.file.path}</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<executions>
<!-- 复制配置文件 -->
<execution>
<id>copy-resources</id>
<phase>package</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<resources>
<resource>
<directory>src/main/resources</directory>
</resource>
</resources>
<outputDirectory>${project.build.directory}/${output.resource.file.path}</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<!--配置jar包特殊标识 配置后,保留原文件,生成新文件 *-run.jar -->
<!--配置jar包特殊标识 不配置,原文件命名为 *.jar.original,生成新文件 *.jar
<classifier>able</classifier> -->
<!--重写包含依赖,包含不存在的依赖,jar里没有pom里的依赖 -->
<includes>
<include>
<groupId>null</groupId>
<artifactId>null</artifactId>
</include>
</includes>
<outputDirectory>${project.build.directory}</outputDirectory>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
遇到的问题
mybatis-plus 自定义TpeHandler失效
现象
有时程序和数据库交互时,需要用到数组类型,所以需要一个类型转换器,实现数据库数组和java数组的互相转换。
一般转换器代码如下:
import java.sql.Array;
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import org.apache.ibatis.type.TypeException;
@MappedTypes({String[].class,Integer[].class,Long[].class,Double[].class,Boolean[].class,})
@MappedJdbcTypes(JdbcType.ARRAY)
public class ArrayType2Handler extends BaseTypeHandler<Object[]> {
private static final String TYPE_NAME_VARCHAR = "varchar";
private static final String TYPE_NAME_INTEGER = "integer";
private static final String TYPE_NAME_BOOLEAN = "boolean";
private static final String TYPE_NAME_NUMERIC = "numeric";
private static final String TYPE_NAME_LONG = "bigint";
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Object[] parameter, JdbcType jdbcType) throws SQLException {
String typeName = null;
if (parameter instanceof Integer[]) {
typeName = TYPE_NAME_INTEGER;
} else if (parameter instanceof String[]) {
typeName = TYPE_NAME_VARCHAR;
} else if (parameter instanceof Boolean[]) {
typeName = TYPE_NAME_BOOLEAN;
} else if (parameter instanceof Double[]) {
typeName = TYPE_NAME_NUMERIC;
}else if(parameter instanceof Long[]){
typeName=TYPE_NAME_LONG;
}
if (typeName == null) {
throw new TypeException("ArrayType2Handler parameter typeName error, your type is " + parameter.getClass().getName());
}
// 这3行是关键的代码,创建Array,然后ps.setArray(i, array)就可以了
Connection conn = ps.getConnection();
Array array = conn.createArrayOf(typeName, parameter);
ps.setArray(i, array);
}
@Override
public Object[] getNullableResult(ResultSet resultSet, String s) throws SQLException {
return getArray(resultSet.getArray(s));
}
@Override
public Object[] getNullableResult(ResultSet resultSet, int i) throws SQLException {
return getArray(resultSet.getArray(i));
}
@Override
public Object[] getNullableResult(CallableStatement callableStatement, int i) throws SQLException {
return getArray(callableStatement.getArray(i));
}
private Object[] getArray(Array array) {
if (array == null) {
return null;
}
try {
return (Object[]) array.getArray();
} catch (Exception e) {
}
return null;
}
}
使用过mybatis的应该知道,这个TypeHandler是在mybatis初始化时加载进去了,但是将fat-jar拆分后,却无法加载自定义的TypeHandler
原因分析
自定义TypeHandler由mybatisplus-spring-boot-starter包中的SpringBootVFS加载,
代码如下:
package com.baomidou.mybatisplus.spring.boot.starter;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import org.apache.ibatis.io.VFS;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
/**
* @author Hans Westerbeek
* @author Eddú Meléndez
* @author Kazuki Shimizu
*/
public class SpringBootVFS extends VFS {
private final ResourcePatternResolver resourceResolver;
public SpringBootVFS() {
this.resourceResolver = new PathMatchingResourcePatternResolver(getClass().getClassLoader());
}
private static String preserveSubpackageName(final URI uri, final String rootPath) {
final String uriStr = uri.toString();
final int start = uriStr.indexOf(rootPath);
return uriStr.substring(start);
}
@Override
public boolean isValid() {
return true;
}
@Override
//此处根据路径加载自定义的TypeHandler
protected List<String> list(URL url, String path) throws IOException {
Resource[] resources = resourceResolver.getResources("classpath*:" + path + "/**/*.class");
List<String> resourcePaths = new ArrayList<String>();
for (Resource resource : resources) {
resourcePaths.add(preserveSubpackageName(resource.getURI(), path));
}
return resourcePaths;
}
}
看代码可知,SpringBootVFS 使用的是spring-core的ResourcePatternResolver类加载类,所以上述问题出在ResourcePatternResolver类,它初始化的代码如下:
this.resourceResolver = new PathMatchingResourcePatternResolver(getClass().getClassLoader());
getClass().getClassLoader()获取的类加载器是当前类,即加载SpringBootVFS 的类加载器。
再接着查找SpringBootVFS 是如何加载的,代码如下:
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean();
factory.setDataSource(dataSource);
//此处引用SpringBootVFS,触发类加载
factory.setVfs(SpringBootVFS.class);
//省略无关代码
return factory.getObject();
}
上述代码是mybatisplus-spring-boot-starter的MybatisPlusAutoConfiguration类中的代码,SpringBootVFS.class的类加载器和MybatisPlusAutoConfiguration相同,由于mybatisplus-spring-boot-starter-xxx.jar已经移动到fat-jar外部,所以mybatisplus-spring-boot-starter-xxx.jar中类的加载使用的是
应用类加载器AppClassLoader,一般名称是sun.misc.Launcher$AppClassLoader,但是它无法加载fat-jar中的类,而自定义的TypeHandler在fat-jar中,所以导致无法加载。
解决方式
修改fat-jar之前,所有的jar和源码都在fat-jar中,fat-jar由springboot组织,由JarLauncher类加载器加载,所以SpringBootVFS 也会使用JarLauncher加载自定义TypeHandler。
自定义TypeHandler在fat-jar中,所以只有springboot 的类加载器JarLauncher才能加载到,SpringBootVFS 肯定无法做到,因此我们需要自定义一个VFS ,使用JarLauncher加载类。
代码如下:
public class CustomSpringBootVFS extends VFS {
private final ResourcePatternResolver resourceResolver;
public CustomSpringBootVFS() {
this.resourceResolver = new PathMatchingResourcePatternResolver(Thread.currentThread()
.getContextClassLoader());
}
private static String preserveSubpackageName(final URI uri, final String rootPath) {
final String uriStr = uri.toString();
final int start = uriStr.indexOf(rootPath);
return uriStr.substring(start);
}
@Override
public boolean isValid() {
return true;
}
@Override
protected List<String> list(URL url, String path) throws IOException {
Resource[] resources = resourceResolver.getResources("classpath*:" + path + "/**/*.class");
List<String> resourcePaths = new ArrayList<String>();
for (Resource resource : resources) {
resourcePaths.add(preserveSubpackageName(resource.getURI(), path));
}
return resourcePaths;
}
}
其实只修改了初始化PathMatchingResourcePatternResolver时传入的类加载,改为当前线程中的类加载器,虽然我们修改了打包方式,但是还是一个springboot的工程,启动时,springboot会将自己的类加载器JarLauncher设置到当前线程中,我们直接使用即可。
此外要在application.properties配置文件中指定我们使用的VFS。
mybatis-plus.configuration.vfs-impl=com.xxx.vfs.CustomSpringBootVFS
至此,完美拆分掉springboot的fat-jar,并修复了出现的问题。