写一个玩具

写一个玩具

核心理念

  1. 以对话的形式
  2. 用摇签的交互方式
  3. 通过信息掮客赚心里按摩的钱

模型设计

用例图

@startuml
left to right direction
skinparam packageStyle rectangle

actor "求问人" as Seeker
actor "解签人" as Interpreter
rectangle "系统" as System {
  usecase "摇晃手机模拟摇卦" as UseCase1
  usecase "向解签人求问卦象" as UseCase2
  usecase "解签人回答并提供咨询" as UseCase3
}

Seeker --> UseCase1
Seeker --> UseCase2
Interpreter --> UseCase3

@enduml

类图

@startuml
interface Role {
  +interpretation(): String
  +shake(): Yao
}

class Account {
  -name: String
  -role: Role
}

class Interpreter implements Role {
  +interpretation(): String
}

class Seeker implements Role {
  +shake(): Yao
}

class DivinationCup {
  +shake(): Yao
}

class Yao {
  -value: Boolean
}

class Hexagram {
  -yas: Yao[6]
  -interpretation: String
  +getSession(theme: String, account: Account): Session
}

class Session {
  -hexagram: Hexagram
  -interpretationRecord: InterpretationRecord
  -theme: String
  -sender: Account
  -receiver: Account
  +sendToInterpreter(): void
}

class InterpretationRecord {
  -interpreterComments: String
  -question: String
  -theme: String
}

Account --> Role : has
Seeker --> DivinationCup : uses
Hexagram --> Yao : contains
Hexagram --> Session : creates
Session --> InterpretationRecord : records

@enduml


非核心业务需求描述

  1. 使用一套代码支持多种类型程序(小程序、APP、H5等)
  2. 使用相同的业务账户
  3. 将收费项目移到可控代码处

技术选型

业务前端

  1. 目前Taro+Taro-ui
  2. 微信小程序
  3. 原生APP

可控代码后端

  1. 选用Java/Go均可

交易系统建模

交易系统建模

平台视角

业务域

  • 聚合根
    • 管理员/商家/采购员
    • 询价单
    • 交易订单
    • 交易对手方
    • 交易时间
    • 交易内容
      • 出库单/入库单
      • 库存
      • 批次
      • 数量
    • 履约单
      • 物流单
      • 仓储单
      • 配送单
    • 售后单
    • 摊位信息
    • 图片
    • 简介
    • 商品列表
    • 广告位
    • 结算单

支撑域

  • 技术支撑
  • 外部接入
  • 对外服务

管理域

  • 工单
  • 入驻审核单

建模图片

翻新系统实践

翻新系统实践

基本情况

  • freemark+jsp+java14的怪异组合
  • 有部分内部接口采用feign
  • 大部分接口采用nacos作为profile管理以及服务注册发现
  • 前端部分仅使用了以jquery和easyui为主的插件,工程化程度不高

需求说明

来自于上线时候的苦恼,每次更新代码总会引发未在版本规划内的前端代码提交,且升级时间较长,失败回滚时候还有cache问题

问题分析

  1. 交互代码和逻辑代码均由同一个开发人员完成,此时心智负担较大,在工期较为短的时候,非常容易造成误操作,且只有上线后才能发现
  2. 系统经历了十年左右的开发,每个开发人员有不同的技术栈喜好,例如早期约定使用Spring Cloud的早期版本,而在五年后发现Spring Cloud很多功能又要二次开发,转向了nacos为主的国产。成员变换中不习惯,有部分接口就干脆采取域名+Nginx网关直连的形式
  3. 除此之外业务系统的拧巴逻辑也是翻新的重大障碍

方案选择

经过和开发小组成员讨论,建议先从增量做起,避开旧有尚能完备运行的业务,其中最简单的是需要抽出一个专门的开发人力,进行前端框架的搭建。(目前使用vue2,开发人员易于招聘)

框架需要完成以下几个事项:

  1. 要替换掉原来依靠服务器进行状态管理的现状,将状态管理放在前端
  2. 要将功能路由管理放在前端框架进行管理
  3. 要维护一个列表,标记哪些路由是用于直接走原来的逻辑,哪些路由是新增的有前端管理的逻辑
  4. 需要后端软件新增接口用以维护新旧软件的状态(一次性开发成本)

在前端框架开发完成一个月后,选取更新频率最低的应用作为试点应用,避免与正在开发的任务同时进行

当前后端代码可以剥离完毕后,在考虑后端的技术栈提纯,不应将改造任务混杂

一些结局

  1. 成功改造了从外包厂商回收的代码
  2. 在重要业务中,成功替换了进门抬杆页面的代码
  3. 为后续的业务不中断升级铺平道路

证券、信息聚合系统框架与搭建(三)

证券、信息聚合系统框架与搭建

问题

  1. 现有的所有的渲染都放在客户端,服务端只有数据
  2. 客户端为了新增图表都需要更新客户端
  3. 目标用户更新客户端意图不强烈

扩大视角范围

不再只关注渲染层的事项,关注整个富客户端的架构

参考

参考后台开发组件化的趋势,做一个大而完整的软件平台

考虑到是软件平台,必须要解耦所有组件之间的直接调用关系

基础组件构成

  • 组件注册中心
  • 组件间通信工具(消息队列、消息总线)
  • 本地数据库
  • 渲染容器
  • 与行情交易柜台对接的各容器适配器

软件组件划分

高频变动数据,低变更

原生程序开发

低频数据

使用Chromium做Web开发,降低开发成本,同时因为Web软件的天然容易更新的属性,能够在变更时候不需要客户端更新基础组件

组件Example

证券,信息聚合前端框架设计与搭建(二)

证券,信息聚合前端框架设计与搭建(二)

技术选型

现有的React可能比较符合需求,但react的diff&patch是O(n^3)级别的算法,这里可能会限制我们单页Component数量

UI界面的话,可以任意选用各个大厂的UI,例如ant.design(PC)ele.me(手机H5)

另外可以自然的引入React Native

但不应该绑死在React技术栈上,防止其授权失效直接导致业务下线

实现路径

早期验证业务阶段可以自行封装Component,系统内部采用封装后的Component来进行代码的编写(Wrap)

等开发中常见的工具类,常见的Class均稳定后,替换Component的内部实现,可以考虑重新通过开源的virtual dom库来实现

设立低时延展示组件

完全避开react的渲染逻辑

手动Bind数据,手动确认渲染时刻并将其渲染时机封装在Component下,避免当高频数据到来时CPU全部分配至JS引擎来处理差分数据合并而Render线程被阻塞

逐步封装Table,List等组件

证券,信息聚合前端框架设计与搭建

证券,信息聚合前端框架设计与搭建

主旨

工程上

  • 减少前端文件的大小
  • 方便更新版本
  • 最好是单页面应用,减少sso成本
  • 代码混淆

通信性能要求

对于高频信息,应以websocket为主,主要需要解决的问题是从服务器到达客户端的延时抖动,以及TCP的延时断线问题

对于中频信息,应以定时任务刷新即可满足

若是采用差分数据方法,可以减少网络IO消耗

高频信息(ms级别)

  • L2交易数据
  • L1行情更新

中频信息(分钟级别)

  • IM
  • 公司公告

渲染性能要求

优先级高的,高频信息,应尽可能快的渲染(60FPS)(16.67ms)内合并vDom事件然后渲染

中频信息按需渲染

应给调用者refresh接口,方便业务可以直接触发更新界面事件

鸿蒙读书笔记

鸿蒙读书笔记

核心系统

kernel基本定义了线程,内存,基础数据结构的公共部分

是其他组件的基石

losbase

定义了基本的信号

定义了内存运算接口

定义了内存对齐接口

定义了休眠接口

bitmap

一些bitmap的运算

bitmap的数据结构

cpup

cpu使用量统计

err

err的IO定义于,err的定义

event

系统事件定义,及调度函数

其他

一些通用的数据结构定义

例如

  • 几种常见的锁
  • NodeList
  • mux
  • 系统类型
  • 队列
  • 进程结构

Socket编程实践(二)

Socket编程实践(二)

最近新任务,需要写一个Socket服务端,用来接收设备透传数据

考虑以下几点

  1. 设备规模(300~2000)
  2. 链接方式(长连接)
  3. 文本协议未定
  4. 有持久化需求
  5. 有前端控制页面

方案选型

  • 因为涉及有持久化需求,应该都是数据库相关操作,直接使用SpringBoot套件
  • 因为有前端控制需求,直接添加SpringMVC作为控制面板开发套件
  • 因为设备规模仅有2000个且是长连接,直接使用BIO协议开发效率更高(更推荐Netty来做开发模型,可惜不太熟悉)

类设计

以下三个类代表了领域模型核心,其他功能均基于以下三个对象做开发或辅助

Device

代表了具体的一个设备

  1. 设备验证与登录
  2. 设备状态维护
  3. 设备数据接收
  4. 设备数据维护与持久化

DevicePool

代表了设备们的管理功能,如

  1. 监测在线
  2. 监测是否有数据到达
  3. 设备上线下线处理

Waitress

代表了SocketServer接待处

仅用来分发接收到的socket然后交给线程池就不管事了

如何构建回测系统(十)

如何构建回测系统(十)

问题

在(九)中实现了一个可以从数据库中编译并加载代码的classloader,但这是有缺陷的

缺陷在于,如果要使用回测系统实现的类和对象则会出现编译无法通过

提示Symbol找不到或者缺失

定位问题

日常我们使用编译功能一般都是maven或者是ant等框架来编译,很少说直接用javac来直接对整个项目编译

在(九)使用的compiler其实就是javac的java代码版本如果采用javac编译一个引用了其他java或者class的文件,则会出现很类似的症状

找到Maven/IDE等工具具体执行javac的代码其实就能看到,是编译时候带上了参数-cp/-classpath/

解决问题

compiler的getTask函数中有个参数叫options,根据文档,这是类似于javac中的参数,但其实如果我们简单的把依赖的class按照-cp的形式添加到参数中就会发现直接报错

这是因为-cp是传给jvm的参数,getTask函数只支持具体的54个参数,属于编译期间具体的信息数据用的

而-cp产生作用的方式是在FileManager中的list()函数起作用的(这里完全没有任何文档,只能单步调试代码发现,所以这里无法说以后就一定正确)

我们只需要将依赖的class/jar/java文件按照需求变更成SimpleJavaFileObject既可

参考代码

因为环境因素,这里的参考代码是在springboot中实现的

package com.michaelssss.domain.strategy;

import static javax.tools.StandardLocation.CLASS_PATH;

import com.michaelssss.exception.RunException;
import com.michaelssss.utils.ArrayUtils;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import javax.tools.DiagnosticCollector;
import javax.tools.FileObject;
import javax.tools.ForwardingJavaFileManager;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileManager;
import javax.tools.JavaFileObject;
import javax.tools.JavaFileObject.Kind;
import javax.tools.SimpleJavaFileObject;
import javax.tools.ToolProvider;
import org.springframework.stereotype.Component;

@Component
public class MyCompiler {

    public byte[] compile(String name, String codes) {
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        var diagnosticCollector = new DiagnosticCollector<JavaFileObject>();
        var fileManager = new MyJavaFileObjectManager(
            compiler.getStandardFileManager(diagnosticCollector, Locale.CHINESE,
                StandardCharsets.UTF_8));
        fileManager.complieName = name;
        var javaFileObjects = Collections
            .singletonList(
                new MyJavaFileObject(name.replace('.', '/'), codes, fileManager.outputStream));
        var options = new ArrayList<String>();
        var compilerTask = compiler
            .getTask(null, fileManager, diagnosticCollector, options, null, javaFileObjects);
        var result = compilerTask.call();
        if (Objects.nonNull(result) && result) {
            return fileManager.outputStream.toByteArray();
        } else {
            StringBuilder stringBuilder = new StringBuilder();
            diagnosticCollector.getDiagnostics()
                .forEach(diagnostic -> stringBuilder.append(
                    diagnostic.toString()
                ).append("\r\n"));
            throw new RunException(stringBuilder.toString());
        }
    }

    static class MyJavaFileObjectManager extends ForwardingJavaFileManager<JavaFileManager> {

        Map<String, JavaFileObject> fileObjects = new HashMap<>();
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        List<JavaFileObject> result;
        String complieName;

        public MyJavaFileObjectManager(JavaFileManager fileManager) {
            super(fileManager);
        }

        @Override
        public JavaFileObject getJavaFileForInput(Location location, String className,
            Kind kind) throws IOException {
            JavaFileObject javaFileObject = fileObjects.get(className);
            if (!location.getName().contains(CLASS_PATH.getName())) {
                return super.getJavaFileForInput(location, className, kind);
            }
            if (Objects.isNull(javaFileObject)) {
                if (Objects.isNull(result)) {
                    result = CompilerDependenceResolver.resolve();
                }
                var aa = result.stream().filter(
                    f -> f.getName().equalsIgnoreCase(className) && kind.equals(f.getKind()))
                    .collect(
                        Collectors.toList());
                if (aa.isEmpty()) {
                    return super.getJavaFileForInput(location, className, kind);
                } else {
                    return aa.get(0);
                }
            }
            return javaFileObject;
        }

        @Override
        public JavaFileObject getJavaFileForOutput(Location location, String qualifiedClassName,
            JavaFileObject.Kind kind, FileObject sibling) throws IOException {
            JavaFileObject javaFileObject = new MyJavaFileObject(qualifiedClassName, kind,
                outputStream);
            fileObjects.put(qualifiedClassName, javaFileObject);
            return javaFileObject;
        }

        @Override
        public String inferBinaryName(Location location, JavaFileObject file) {
            if (file instanceof MyJavaFileObject) {
                return file.getName();
            }
            if (file instanceof WithPackageAccessSimplaJava) {
                return ((WithPackageAccessSimplaJava) file).getBinaryName();
            }
            if (
                file instanceof NewJarJavaFileObject
            ) {
                return ((NewJarJavaFileObject) file).getBinaryName();
            } else {
                return super.inferBinaryName(location, file);
            }
        }

        @Override
        public Iterable<JavaFileObject> list(Location location, String packageName, Set<Kind> kinds,
            boolean recurse) throws IOException {

            if (location.getName().contains(CLASS_PATH.getName())) {
                if (Objects.isNull(result)) {
                    result = CompilerDependenceResolver.resolve();
                }
                if (!result.isEmpty()) {
                    var a = result.stream()
                        .filter(
                            r -> ArrayUtils
                                .isContain(((MySimpleJavaObjectI) r).getBinaryName().split("\\."),
                                    packageName.split("\\.")) && kinds
                                .contains(r.getKind()))
                        .collect(Collectors.toList());
                    var e = super.list(location, packageName, kinds, recurse);
                    e.forEach(a::add);
                    return a;
                }
            }
            return super.list(location, packageName, kinds, recurse);
        }
    }


    static class MyJavaFileObject extends SimpleJavaFileObject {

        String source;
        OutputStream outputStream;
        String name;

        public MyJavaFileObject(String name, Kind kind, OutputStream stream) {
            super(URI.create("String:///" + name + kind.extension), kind);
            this.outputStream = stream;
        }

        public MyJavaFileObject(String name, String source) {
            super(URI.create("String:///" + name + Kind.SOURCE.extension), Kind.SOURCE);
            this.source = source;
            this.name = name;
        }

        public MyJavaFileObject(String name, String source, OutputStream outputStream) {
            this(name, source);
            this.outputStream = outputStream;
        }

        public MyJavaFileObject(URI uri, Kind kind) {
            super(uri, kind);
        }

        @Override
        public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
            return source;
        }

        @Override
        public OutputStream openOutputStream() throws IOException {
            return outputStream;
        }
    }
}

package com.michaelssss.domain.strategy;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URI;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.jar.JarFile;
import java.util.stream.Collectors;
import javax.tools.JavaFileObject;
import javax.tools.JavaFileObject.Kind;
import lombok.SneakyThrows;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.system.ApplicationHome;

public class CompilerDependenceResolver {

    /**
     * @return 返回所有非SystemLoader加载的class的JavaFileObject
     */
    public static List<String> getClassPath() {
        var systemType = System.getProperty("os.name");

        var classpath = new String[0];
        if (systemType.contains("Windows")) {
            classpath = System.getProperty("java.class.path").split(";");
        }
        if (systemType.contains("Linux")) {
            classpath = System.getProperty("java.class.path").split(":");
        }
        return Arrays.asList(classpath);
    }

    @SneakyThrows
    public static List<JavaFileObject> resolve() {
        List<JavaFileObject> result = new ArrayList<>();
        //查询所有非springboot主文件本身的jar
        final List<String> jarlibs = new ArrayList<>();
        var loadPath = System.getProperty("loader.path");
        var classpath = System.getProperty("java.class.path").split(";");
        if (StringUtils.isNotEmpty(loadPath)) {
            classpath = Files.list(Path.of(loadPath))
                .filter(path -> !Files.isDirectory(path) && path.toString().endsWith(".jar"))
                .map(Path::toString).toArray(String[]::new);
        }
        jarlibs.addAll(Arrays.stream(classpath).filter(s -> s.endsWith(".jar"))
            .collect(Collectors.toList()));
        var compilerModuleClassPaths = Arrays.stream(classpath).filter(s -> !s.endsWith(".jar"))
            .collect(Collectors.toList());

        if (jarlibs.isEmpty()) {
            throw new Error("load jar info failed");
        }
        jarlibs.forEach(s -> {
            try {
                JarFile jarFile = new JarFile(s);
                jarFile
                    .stream()
                    .forEach(jarEntry -> {
                        if (!jarEntry.isDirectory() && jarEntry.getName().endsWith(".class")) {
                            try (var in = jarFile.getInputStream(jarEntry)) {
                                var jarPath = Path.of(s);
                                String separator = jarEntry.getName().startsWith("/") ? "!" : "!/";
                                ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
                                IOUtils.copy(in, outputStream);
                                result.add(
                                    new NewJarJavaFileObject(
                                        URI.create(
                                            "jar:///" + jarPath.toUri().normalize() + separator
                                                + jarEntry
                                                .getName()),
                                        Kind.CLASS,
                                        outputStream,
                                        jarEntry
                                    ));
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                    });

            } catch (IOException e) {
                e.printStackTrace();
            }

        });
        if (!isJar()) {
            var path = Path.of(getPath());
            Files.walkFileTree(path, new FileVisitor<Path>() {
                @Override
                public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
                    throws IOException {
                    return FileVisitResult.CONTINUE;
                }

                @Override
                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
                    throws IOException {
                    if (file.toString().endsWith(".class")) {

                        WithPackageAccessSimplaJava withPackageAccessSimplaJava = new WithPackageAccessSimplaJava(
                            path.toUri(),
                            Kind.CLASS, path.normalize().toString(), file);
                        result.add(withPackageAccessSimplaJava);
                    }
                    return FileVisitResult.CONTINUE;
                }

                @Override
                public FileVisitResult visitFileFailed(Path file, IOException exc)
                    throws IOException {
                    return FileVisitResult.CONTINUE;
                }

                @Override
                public FileVisitResult postVisitDirectory(Path dir, IOException exc)
                    throws IOException {
                    return FileVisitResult.CONTINUE;
                }
            });
            compilerModuleClassPaths.forEach(path1 -> {
                try {
                    Files.walkFileTree(Path.of(path1), new FileVisitor<Path>() {
                        @Override
                        public FileVisitResult preVisitDirectory(Path dir,
                            BasicFileAttributes attrs)
                            throws IOException {
                            return FileVisitResult.CONTINUE;
                        }

                        @Override
                        public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
                            throws IOException {
                            if (file.toString().endsWith(".class")) {
                                WithPackageAccessSimplaJava withPackageAccessSimplaJava = new WithPackageAccessSimplaJava(
                                    Path.of(path1).toUri(),
                                    Kind.CLASS, path1, file);
                                result.add(withPackageAccessSimplaJava);
                            }
                            return FileVisitResult.CONTINUE;
                        }

                        @Override
                        public FileVisitResult visitFileFailed(Path file, IOException exc)
                            throws IOException {
                            return FileVisitResult.CONTINUE;
                        }

                        @Override
                        public FileVisitResult postVisitDirectory(Path dir, IOException exc)
                            throws IOException {
                            return FileVisitResult.CONTINUE;
                        }
                    });
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
        } else {
            var springbootjarpath = Arrays.stream(System.getProperty("java.class.path").split(";"))
                .filter(s -> s.endsWith(".jar"));
            var tempPath = Path.of(getPath()).getParent().resolve("temp");
            if (Files.notExists(tempPath)) {
                Files.createDirectories(tempPath);
            }
            springbootjarpath.forEach(s -> {
                try {
                    //直接解压

                    var jarFile = new JarFile(s);
                    jarFile.stream().filter(jarEntry -> jarEntry.getName().contains("BOOT-INF"))
                        .filter(jarEntry -> !jarEntry.isDirectory())
                        .forEach(jarEntry -> {
                            try {
                                if (Files
                                    .notExists(tempPath.resolve(jarEntry.getName()).getParent())) {
                                    Files.createDirectories(
                                        tempPath.resolve(jarEntry.getName()).getParent());
                                }

                                Files.deleteIfExists(tempPath.resolve(jarEntry.getName()));
                                var filePath = Files
                                    .createFile(tempPath.resolve(jarEntry.getName()));

                                var ou = Files.newOutputStream(filePath);
                                var in = jarFile.getInputStream(jarEntry);
                                IOUtils.copy(in, ou);
                                in.close();
                                ou.close();
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        });

                } catch (IOException e) {
                    e.printStackTrace();
                }
            });

            Files.walk(tempPath.resolve("BOOT-INF"))
                .filter(f -> !Files.isDirectory(f))
                .forEach(s -> {
                    if (s.toString().endsWith(".jar")) {
                        try {
                            JarFile jarFile = new JarFile(s.toString());
                            jarFile
                                .stream()
                                .forEach(jarEntry -> {
                                    if (!jarEntry.isDirectory() && jarEntry.getName()
                                        .endsWith(".class")) {
                                        try (var in = jarFile.getInputStream(jarEntry)) {
                                            var jarPath = Path.of(s.toString());
                                            String separator =
                                                jarEntry.getName().startsWith("/") ? "!" : "!/";
                                            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
                                            IOUtils.copy(in, outputStream);
                                            result.add(
                                                new NewJarJavaFileObject(
                                                    URI.create(
                                                        "jar:///" + jarPath.toUri().normalize()
                                                            + separator
                                                            + jarEntry
                                                            .getName()),
                                                    Kind.CLASS,
                                                    outputStream,
                                                    jarEntry
                                                ));
                                        } catch (IOException e) {
                                            e.printStackTrace();
                                        }
                                    }
                                });

                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    } else if (s.toString().endsWith(".class")) {
                        WithPackageAccessSimplaJava withPackageAccessSimplaJava = new WithPackageAccessSimplaJava(
                            s.toUri(),
                            Kind.CLASS,
                            tempPath.resolve("BOOT-INF").resolve("classes").normalize().toString(),
                            s);
                        result.add(withPackageAccessSimplaJava);
                    }
                });
        }
        return result;
    }

    public static String getPath() {
        ApplicationHome home = new ApplicationHome(DBClazz.class);
        return home.getSource().getPath();
    }

    /**
     * 判断是否jar模式运行
     */
    public static boolean isJar() {
        return StringUtils.isNotEmpty(System.getProperty("loader.path"));
    }


    public static void main(String[] args) {
        resolve();
    }
}

如何构建回测系统(九)

如何构建回测系统(九)

关于HotReload Classloader的一个实践

在(八)中,我们主要考虑到了一个大的框架是怎么实现,大概有哪些部件,现在来看一下我的参考实现

@Slf4j
public class DBStrategyClassLoader extends ClassLoader {

    private final Map<String, Class<?>> loadClassMap;
    private static DynamicStrategyRepository repository;
    private static DBStrategyClassLoader instance;
    private static ClassLoader springbootClassloader;

    private DBStrategyClassLoader() {
        this.loadClassMap = new ConcurrentHashMap<>();
    }

    public static void init(DynamicStrategyRepository repository,
                            ClassLoader springbootClassloader) {
        DBStrategyClassLoader.repository = repository;
        DBStrategyClassLoader.springbootClassloader = springbootClassloader;
        log.info("DBStrategyClassLoader initialized");
    }

    public void reload() {
        log.info("class reload start");
        synchronized (DBStrategyClassLoader.class) {
            var newInstance = new DBStrategyClassLoader();
            var loadClassName = loadClassMap.keySet();
            for (var clazzName : loadClassName) {
                try {
                    newInstance.loadClassMap.put(clazzName, newInstance.loadClass(clazzName));
                } catch (ClassNotFoundException e) {
                    log.error("reload clazz {}: failed,", clazzName, e);
                }
            }
            instance = newInstance;
        }
        log.info("class reloaded ");
    }

    public static DBStrategyClassLoader getInstance() {
        if (Objects.isNull(instance)) {
            synchronized (DBStrategyClassLoader.class) {
                if (Objects.isNull(instance)) {
                    instance = new DBStrategyClassLoader();
                }
            }
        }
        return instance;
    }

    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        try {
            return springbootClassloader.loadClass(name);
        } catch (ClassNotFoundException e) {
            return super.loadClass(name);
        }
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        var clazz = this.loadClassMap.get(name);
        if (Objects.isNull(clazz)) {

            var dbData = repository.findByDbClazz_name(name);
            if (!dbData.isEmpty()) {
                var data = dbData.stream().max(Comparator.comparing(DynamicStrategy::getVersion))
                        .get().getDbClazz().compile();
                clazz = defineClass(name, data, 0, data.length);
                loadClassMap.put(name, clazz);
            } else {
                throw new ClassNotFoundException();
            }
        }
        return clazz;
    }
}

由谁管理这个ClassLoader的生命周期

这里有个前置条件,我大部分的组件,为了方便再springmvc中使用,都采用了Spring来做生命周期管理,但是对于classloader,为了完整将classloader在root object上置为null,应交由自己管理

故而在这里使用了双锁单例模式

public static DBStrategyClassLoader getInstance() {
    if (Objects.isNull(instance)) {
        synchronized (DBStrategyClassLoader.class) {
            if (Objects.isNull(instance)) {
                instance = new DBStrategyClassLoader();
            }
        }
    }
    return instance;
}

reload详解

为了使得reload之后,代码能重新运行,还要重新loadclass,当所有准备就绪后,替换instance

同时使用了该classloader的部件应该重新加载Class使得新的代码生效

public void reload() {
    log.info("class reload start");
    synchronized (DBStrategyClassLoader.class) {
        var newInstance = new DBStrategyClassLoader();
        var loadClassName = loadClassMap.keySet();
        for (var clazzName : loadClassName) {
            try {
                newInstance.loadClassMap.put(clazzName, newInstance.loadClass(clazzName));
            } catch (ClassNotFoundException e) {
                log.error("reload clazz {}: failed,", clazzName, e);
            }
        }
        instance = newInstance;
    }
    log.info("class reloaded ");
}

其他

由于项目是SpringBoot的项目,众所周知是Springboot自己也实现了Classloader,所有我们非数据库代码都是通过该Classloader加载的,故而,我们还需要在对象中设置Springboot的Classloader,从任意Spring管理对象然后getClass().getClassloader()即可获得