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,并修复了出现的问题。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值