类隔离机制的方案

1.概述

使用excel导出时,使用的poi版本必须是3,因为要集成进来,所以统一poi的版本为3,目前有需要使用poi5,目前是不支持依赖jar包的隔离机制,需要提供类隔离机制来解决此问题。

2.问题分析

首先基于背景分析现状,现状就是在对应用的某些功能进行升级的时候,会出现包冲突等问题,导致开发以及维护的复杂度增加.

在解决升级问题的时候,主要有两种方案:

当我们直接升级包依赖版本的时候,会导致如下状况:

很明显直接升级的成本是很高的,于是在考虑复杂度跟成本的情况下是尽量不使用这种方案. 微服务化,一个版本一个服务  不是不可以,就是有点杀鸡用牛刀的感觉,除非您的版本影响的业务功能极其庞大

让sdk-1.0与sdk-2.0并存项目,会导致如下状况:

在选择并存的方案的时候,最棘手的就是包冲突的问题,最好的情况就是只需要排除冲突就可以了,但是最坏的情况是因为是同类jar包但不同版本,在引入同一个类的时候,很容易出现java.lang.NoClassDefFoundError和ClassNotFoundException问题.

主要是因为从类加载机制分析,在 Java 中,所有的类默认通过 ClassLoader 加载,而 Java 默认提供了三层的 ClassLoader,并通过双亲委托模型的原则进行加载.

双亲委托模型如下:

双亲委派模型的工作流程是:如果一个类加载器收到了类加载请求,它首先不会自己去尝试加载这个类,而是把这个请求委托给父类加载器去完成,每一个层次的类加载器都是如此。因此,所有的加载请求都应当传送到顶层的BootStrap加载器中,只有当父加载器反馈无法完成这个加载请求时(在自己搜索范围中没有找到此类),子加载器才会尝试自己去加载。

在我们没有对jar包的类加载器进行自定义的时候,我们对jar的类进行调用的时候,会去到默认的父类加载器Bootstrap ClassLoader,加载我们调用的类,由于是同类jar包不同版本,Bootstrap ClassLoader未必会加载到我们想要的类,因此会出现java.lang.NoClassDefFoundError和ClassNotFoundException问题.

针对这个问题,我们需要做的是基于双亲委派模型,自定义classloader使得两个版本的jar包是使用两个不同的classloader,我们调用的时候使用自定义的classloader进行调用就可以实现多个同类但不同版本的jar包并存.

3.类隔离实现方案

3.1.自定义classloader实现类隔离

自定义classloader就是上面说的实现方向,此处不做展开,需要的朋友可以参考:

类加载器隔离朴实案例【重点】【loadclass yetdone】 - silyvin - 博客园

Java 自定义 ClassLoader 实现隔离运行不同版本jar包的方式_threadcontextclassloader实现线程隔离避免包冲突-优快云博客

3.2.使用开源框架

<1>OSGI,框架体系庞大,复杂

<2>基于sofaark实现隔离

4.SOFA-ARK开源框架

SOFAArk 是一款基于 Java 实现的轻量级类隔离容器,主要提供类隔离和应用(模块)合并部署能力,由蚂蚁金服公司开源贡献;

在大型软件开发过程中,通常会推荐底层功能插件化,业务功能模块化的开发模式,以期达到低耦合、高内聚、功能复用的优点。基于此,SOFAArk 提供了一套较为规范化的插件化、模块化的开发方案,产品能力主要包括:

  • 定义类加载模型,运行时底层插件、业务应用(模块)之间均相互隔离,单一插件和应用(模块)由不同的 ClassLoader 加载,可以有效避免相互之间的包冲突,提升插件和模块功能复用能力;

  • 定义插件开发规范,提供 maven 打包工具,简单快速将多个二方包打包成插件(Ark Plugin,以下简称 Plugin)

  • 定义模块开发规范,提供 maven 打包工具,简单快速将应用打包成模块 (Ark Biz,以下简称 Biz)

  • 针对 Plugin、Biz 提供标准的编程界面,包括服务、事件、扩展点等机制

  • 支持多 Biz 的合并部署,开发阶段将多个 Biz 打包成可执行 Fat Jar,或者运行时使用 API 或配置中心(Zookeeper)动态地安装卸载 Biz

基于以上能力,SOFAArk 可以帮助解决依赖包冲突、多应用(模块)合并部署等场景问题

4.1.场景

4.1.1.包冲突

日常使用 Java 开发,常常会遇到包依赖冲突的问题,尤其当应用变得臃肿庞大,包冲突的问题也会变得更加棘手,导致各种各样的报错,例如 LinkageError, NoSuchMethodError 等;实际开发中,可以采用多种方法来解决包冲突问题,比较常见的是类似 Spring Boot 的做法,统一管理应用所有依赖包的版本,保证这些三方包不存在依赖冲突;这种做法只能有效避免包冲突问题,不能根本上解决包冲突的问题;如果某个应用的确需要在运行时使用两个相互冲突的包,例如 protobuf2protobuf3,那么类似 Spring Boot 的做法依然解决不了问题。

为了彻底解决包冲突的问题,需要借助类隔离机制,使用不同的 ClassLoader 加载不同版本的三方依赖,进而隔离包冲突问题; OSGI 作为业内最出名的类隔离框架,自然是可以被用于解决上述包冲突问题,但是 OSGI 框架太过臃肿,功能繁杂;为了解决包冲突问题,引入 OSGI 框架,有牛刀杀鸡之嫌,且反而使工程变得更加复杂,不利于开发;

SOFAArk 采用轻量级的类隔离方案来解决日常经常遇到的包冲突问题,在蚂蚁金服内部服务于整个 SOFABoot 技术体系,弥补 Spring Boot 没有的类隔离能力。SOFAArk 提出了一种特殊的包结构 – Ark Plugin,在遇到包冲突时,用户可以使用 Maven 插件将若干冲突包打包成 Plugin,运行时由独立的 PluginClassLoader 加载,从而解决包冲突。

假设如下场景,如果工程需要引入两个三方包:A 和 B,但是 A 需要依赖版本号为 0.1 的 C 包,而恰好 B 需要依赖版本号为 0.2 的 C 包,且 C 包的这两个版本无法兼容:

此时,即可使用 SOFAArk 解决该依赖冲突问题;只需要把 A 和版本为 0.1 的 C 包一起打包成一个 Ark 插件,然后让应用工程引入该插件依赖即可;

4.1.2.合并部署

复杂项目通常需要跨团队协作开发,各自负责不同的组件,而众所周知,协调跨团队合作开发会遇到不少问题;比如各自技术栈不统一导致的依赖冲突,又比如往同一个 Git 仓库提交代码常常导致 merge 冲突。因此,如果能让每个团队将负责的功能组件当成一个个单独的应用开发,运行时合并部署,通过统一的编程界面交互,那么将极大的提升开发效率及应用可扩展性。SOFAArk 提出了一种特殊的包结构 – Ark Biz,用户可以使用 Maven 插件将应用打包成 Biz,允许多 Biz 在 SOFAArk 容器之上合并部署,并通过统一的编程界面交互。

4.1.2.1.静态合并部署

SOFAArk 提供了静态合并部署能力,在开发阶段,应用可以将其他应用打成的 Biz 包通过 Maven 依赖的方式引入,而当自身被打成可执行 Fat Jar 时,可以将其他应用 Biz 包一并打入,启动时,则会根据优先级依次启动各应用。每个 Biz 使用独立的 BizClassLoader 加载,不需要考虑相互依赖冲突问题,Biz 之间则通过 SofaService/SofaReference JVM 服务进行交互。

4.1.2.2.动态合并部署

动态合并部署区别于静态合并部署最大的一点是,运行时通过 API 或者配置中心(Zookeeper)来控制 Biz 的部署和卸载。动态合并部署的设计理念图如下:

无论是静态还是动态合并部署都会有宿主应用(master biz)的概念, 如果 Ark 包只打包了一个 Biz,则该 Biz 默认成为宿主应用;如果 Ark 包打包了多个 Biz 包,需要配置指定宿主应用。宿主应用不允许被卸载,一般而言,宿主应用会作为流量入口的中台系统,具体的服务实现会放在不同的动态 Biz 中,供宿主应用调用。宿主应用可以使用 SOFAArk 提供的客户端 API 实现动态应用的部署和卸载。除了 API, SOFAArk 提供了 Config Plugin,用于对接配置中心(目前支持 Zookeeper),运行时接受动态配置;Config Plugin 会解析下发的配置,控制动态应用的部署和卸载。

4.2.原理

OFAArk 包含三个概念,Ark Container, Ark PluginArk Biz; 运行时逻辑结构图如下:

在介绍这三个概念之前,先介绍上述 Ark 包概念;Ark 包是满足特定目录格式要求的可运行 Fat Jar,使用官方提供的 Maven 插件 sofa-ark-maven-plugin 可以将单个或多个应用打包成标准格式的 Ark 包;使用 java -jar 命令即可在 SOFAArk 容器之上启动所有应用;Ark 包通常包含 Ark ContainerArk PluginArk Biz;以下我们针对这三个概念简单做下名词解释:

  • Ark Container: SOFAArk 容器,负责 Ark 包启动运行时的管理;Ark PluginArk Biz 运行在 SOFAArk 容器之上;容器具备管理插件和应用的功能;容器启动成功后,会自动解析 classpath 包含的 Ark PluginArk Biz 依赖,完成隔离加载并按优先级依次启动之;

  • Ark Plugin: Ark 插件,满足特定目录格式要求的 Fat Jar,使用官方提供的 Maven 插件 sofa-ark-plugin-maven-plugin 可以将一个或多个普通的 Java jar 打包成一个标准格式的 Ark PluginArk Plugin 会包含一份配置文件,通常包括插件类导入导出配置、资源导入导出配置、插件启动优先级等;运行时,SOFAArk 容器会使用独立的 PluginClassLoader加载插件,并根据插件配置构建类加载索引表、资源加载索引表,使插件和插件之间、插件和应用之间相互隔离;

  • Ark Biz: Ark 应用模块,满足特定目录格式要求的 Fat Jar,使用官方提供的 Maven 插件 sofa-ark-maven-plugin 可以将工程应用打包成一个标准格式的 Ark BizArk Biz 是工程应用以及其依赖包的组织单元,包含应用启动所需的所有依赖和配置;一个 Ark 包中可以包含多个 Ark Biz 包,按优先级依次启动,Biz 之间通过 JVM 服务交互;

运行 Ark 包,Ark Container 优先启动,容器自动解析 Ark 包中含有的 Ark PluginArk Biz,并读取他们的配置信息,构建类和资源的加载索引表;然后使用独立的 ClassLoader 加载并按优先级配置依次启动;需要指出的是,Ark Plugin 优先 Ark Biz 被加载启动;Ark Plugin 之间是双向类索引关系,即可以相互委托对方加载所需的类和资源;Ark PluginArk Biz 是单向类索引关系,即只允许 Ark Biz 索引 Ark Plugin 加载的类和资源,反之则不允许。

5.poi版本隔离

sofa-ark版本为2.2.3

5.1.封装poi版本插件

统一poi版本,比如poi3,若业务有需要使用其他版本的poi,可将此poi版本封装为ark插件

5.1.1.创建hessian4-ark-plugin的maven项目

<1>pom.xml配置

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    
    <modelVersion>4.0.0</modelVersion>
​
    <artifactId>hessian4-ark-plugin</artifactId>
    <groupId>com.alipay.sofa</groupId>
    <version>1.0.0</version>
​
    <properties>
        <poi5.version>5.2.3</poi5.version>
        <sofa-ark.version>2.2.3</sofa-ark.version>
    </properties>
​
    <dependencies>
​
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi</artifactId>
            <version>${poi5.version}</version>
        </dependency>
​
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml</artifactId>
            <version>${poi5.version}</version>
        </dependency>
​
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-scratchpad</artifactId>
            <version>${poi5.version}</version>
        </dependency>
​
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml-full</artifactId>
            <version>${poi5.version}</version>
        </dependency>
​
    </dependencies>
​
    <build>
        <plugins>
​
            <plugin>
                <groupId>com.alipay.sofa</groupId>
                <artifactId>sofa-ark-plugin-maven-plugin</artifactId>
                <version>${sofa-ark.version}</version>
                <executions>
                    <execution>
                        <id>default-cli</id>
                        <goals>
                            <goal>ark-plugin</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <attach>true</attach>
                    <!-- configure exported class -->
                    <exported>
                        <!-- configure class-level exported class -->
                        <classes>
                            <class>com.alipay.sofa.demo.hessian4.Hessian4Service</class>
                        </classes>
                    </exported>
                </configuration>
            </plugin>
​
        </plugins>
    </build>
​
</project>

<2>Hessian4Service

/**
 * Alipay.com Inc.
 * Copyright (c) 2004-2018 All Rights Reserved.
 */
package com.alipay.sofa.demo.hessian4;
​
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URL;
import java.security.CodeSource;
​
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.DataFormat;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import com.alipay.sofa.demo.pojo.SamplePoJo1;
import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
​
/**
 * 导出的类
 */
public class Hessian4Service {
​
    //导出的方法
    public byte[] exportExcelPoi5() {
        printLoadClass(HSSFWorkbook.class);
        try (Workbook workbook = new HSSFWorkbook()) {
            // 创建工作表
            Sheet sheet = workbook.createSheet("Sheet1");
​
            // 创建自定义数据格式
            DataFormat dataFormat = workbook.createDataFormat();
            short decimalFormatIndex = dataFormat.getFormat("#,##0.00");
​
            // 设置特定列的数据格式
            CellStyle numericCellStyle = workbook.createCellStyle();
            numericCellStyle.setDataFormat(decimalFormatIndex);
​
            // 创建表头
            Row headerRow = sheet.createRow(0);
            Cell headerCell1 = headerRow.createCell(0);
            headerCell1.setCellValue("姓名");
​
            Cell headerCell2 = headerRow.createCell(1);
            headerCell2.setCellValue("年龄");
​
            Cell headerCell3 = headerRow.createCell(2);
            headerCell3.setCellValue("工资");
​
            // 创建数据行
            Row dataRow1 = sheet.createRow(1);
            Cell dataCell1 = dataRow1.createCell(0);
            dataCell1.setCellValue("John Doe");
​
            Cell dataCell2 = dataRow1.createCell(1);
            dataCell2.setCellValue(30);
​
            Cell dataCell3 = dataRow1.createCell(2);
            dataCell3.setCellValue(5000);
            dataCell3.setCellStyle(numericCellStyle); // 应用自定义格式
​
            // 设置自动列宽
            sheet.autoSizeColumn(0);
            sheet.autoSizeColumn(1);
            sheet.autoSizeColumn(2);
​
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            workbook.write(bos);
​
            return bos.toByteArray();
​
        } catch (IOException e) {
            throw new RuntimeException(e.getMessage());
        }
    }
​
     private static void printLoadClass(Class clazz) {
System.err.println(clazz.getName() + " classLoader:" + clazz.getClassLoader());
​
        CodeSource codeSource = clazz.getProtectionDomain().getCodeSource();
​
        if (codeSource != null) {
            URL jarUrl = codeSource.getLocation();
            System.err.println(clazz.getName() + " Class is in JAR: " + jarUrl.getPath());
        } else {
            System.err.println((clazz.getName() + " Class is not in a JAR file"));
        }
    }
​
}
5.1.2.使用hessian4-ark-plugin

<1>pom.xml中增加以下依赖配置

<dependencies>
  <dependency>
    <groupId>com.alipay.sofa</groupId>
    <artifactId>hessian4-ark-plugin</artifactId>
    <version>1.0.0</version>
  </dependency>
​
  <!-- idea中启动时才需要 -->
  <dependency>
    <groupId>com.alipay.sofa</groupId>
    <artifactId>sofa-ark-springboot-starter</artifactId>
    <version>${sofa-ark.version}</version>
    <scope>provided</scope>
  </dependency>
</dependencies>

<2>pom.xml增加sofa-ark-maven-plugin的打包插件

<plugins>
   <--注释spring-boot-maven-plugin插件,使用sofa-ark-maven-plugin插件>
   <plugin>
            <groupId>com.alipay.sofa</groupId>
            <artifactId>sofa-ark-maven-plugin</artifactId>
            <version>${sofa-ark.version}</version>
​
                <executions>
                    <execution>
                        <id>default-cli</id>
​
                        <!--goal executed to generate executable-ark-jar -->
                        <goals>
                            <goal>repackage</goal>
                        </goals>
​
                        <configuration>
                            <!--specify destination where executable-ark-jar will be saved, default saved to ${project.build.directory}-->
                            <outputDirectory>./target</outputDirectory>
​
                            <!--default none-->
                            <arkClassifier>executable-ark</arkClassifier>
​
                            <packageProvided>false</packageProvided>
                        </configuration>
                    </execution>
​
​
                </executions>
            </plugin>
</plugins>

<3>增加测试类

@RestController
public class HelloController {
    
    private static final String DOWNLOAD_EXCEL_CONTENT_TYPE = "application/vnd.ms-content;charset=UTF-8";
​
    @RequestMapping("/hello-poi5")
    public ResponseEntity<Resource> helloPoi5() {
​
        Hessian4Service hessian4Service = new Hessian4Service();
        return createResourceResponse("test5.xls",  hessian4Service.exportExcelPoi5(), DOWNLOAD_EXCEL_CONTENT_TYPE);
    }
​
    @RequestMapping("/hello-poi3")
    public ResponseEntity<Resource> helloPoi3() {
        return createResourceResponse(System.currentTimeMillis() + "test3.xls",
            exportExcelPoi(), DOWNLOAD_EXCEL_CONTENT_TYPE);
    }
​
    private static ResponseEntity<Resource> createResourceResponse(String fileName,
                                                                   byte[] content,
                                                                   String contentType) {
        HttpHeaders responseHeaders = new HttpHeaders();
        responseHeaders.add("Access-Control-Max-Age", "1800");
        responseHeaders.add("Access-Control-Expose-Headers", "Content-Disposition");
        responseHeaders.add(HttpHeaders.CONTENT_DISPOSITION, contentDisposition(fileName));
        return ResponseEntity.ok()
            .headers(responseHeaders)
            .contentType(MediaType.parseMediaType(contentType))
            .body(new ByteArrayResource(content));
    }
​
    private static final String DOWNLOAD_CONTENT_DISPOSITION = "attachment;filename=%s";
​
    private static String contentDisposition(String fileName) {
        String encodeFileName = UrlUtil.urlEncode(fileName);
        return String.format(DOWNLOAD_CONTENT_DISPOSITION, encodeFileName);
    }
​
    public byte[] exportExcelPoi() {
        printLoadClass(HSSFWorkbook.class);
        try (Workbook workbook = new HSSFWorkbook()) {
            // 创建工作表
            Sheet sheet = workbook.createSheet("Sheet1");
​
            // 创建自定义数据格式
            DataFormat dataFormat = workbook.createDataFormat();
            short decimalFormatIndex = dataFormat.getFormat("#,##0.00");
​
            // 设置特定列的数据格式
            CellStyle numericCellStyle = workbook.createCellStyle();
            numericCellStyle.setDataFormat(decimalFormatIndex);
​
            // 创建表头
            Row headerRow = sheet.createRow(0);
            Cell headerCell1 = headerRow.createCell(0);
            headerCell1.setCellValue("姓名");
​
            Cell headerCell2 = headerRow.createCell(1);
            headerCell2.setCellValue("年龄");
​
            Cell headerCell3 = headerRow.createCell(2);
            headerCell3.setCellValue("工资");
​
            // 创建数据行
            Row dataRow1 = sheet.createRow(1);
            Cell dataCell1 = dataRow1.createCell(0);
            dataCell1.setCellValue("John Doe");
​
            Cell dataCell2 = dataRow1.createCell(1);
            dataCell2.setCellValue(30);
​
            Cell dataCell3 = dataRow1.createCell(2);
            dataCell3.setCellValue(5000);
            dataCell3.setCellStyle(numericCellStyle); // 应用自定义格式
​
            // 设置自动列宽
            sheet.autoSizeColumn(0);
            sheet.autoSizeColumn(1);
            sheet.autoSizeColumn(2);
​
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            workbook.write(bos);
​
            return bos.toByteArray();
​
        } catch (IOException e) {
            throw new RuntimeException(e.getMessage());
        }
    }
​
    private static void printLoadClass(Class clazz) {
        System.err.println(clazz.getName() + " classLoader:" + clazz.getClassLoader());
​
        CodeSource codeSource = clazz.getProtectionDomain().getCodeSource();
​
        if (codeSource != null) {
            URL jarUrl = codeSource.getLocation();
            System.err.println(clazz.getName() + " Class is in JAR: " + jarUrl.getPath());
        } else {
            System.err.println((clazz.getName() + " Class is not in a JAR file"));
        }
    }
}
5.1.3.运行并验证

<1>运行

idea中运行,需要增加jvm参数如下:

-Didea.shorten.command.line=jar

cmd运行如下:

java -jar demo-3.0.0-SNAPSHOT-executable-ark.jar

<2>验证

请求/hello-poi3,控制台会打印HSSFWorkbook是从lib包直接加载

请求/hello-poi5,控制台会打印HSSFWorkbook是从插件的lib中加载的

6.附录

深入Spring Boot:实现对Fat Jar jsp的支持_fatjar jsp-优快云博客

SpringBoot中使用jsp的坑-优快云博客

SpringBoot中对JSP的部分研究 · Issue #1 · huayaoyue6/blog · GitHub

ServletContext getResourceAsStream for file in META-INF/resources does not work in an IDE, spring-boot:run, or bootRun · Issue #8525 · spring-projects/spring-boot · GitHub

https://zhuanlan.zhihu.com/p/36909393

https://blog.youkuaiyun.com/qq_28540443/article/details/106399367

GitHub - sofastack-guides/sofa-ark-class-isolation: 通过多plugin解决类冲突问题

GitHub - sofastack-guides/sofa-ark-guides: SOFAArk 工程用例

静态合并部署 | SOFAServerless

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

tof21

支持原创

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值