bitkylin

bitkylin 查看完整档案

上海编辑桂林电子科技大学  |  电子与通信工程 编辑上海数禾信息科技有限公司  |  全栈向程序猿 编辑 bitky.cc/ 编辑
编辑
  1. 项目经验丰富:研究生期间主导并参与多个项目,包括多个Android、.NET项目,一个全栈项目,多个准全栈项目。已有一款产品投入量产,多款产品处于研发状态。
  2. 科技竞赛屡获奖项:多次参加国家级、省级的研究生电子设计竞赛、”挑战杯”、数学建模竞赛并获奖。
  3. 学习、科研成绩优异:学习成绩名列前茅,排名保持在前10%,已申请多项专利和软件著作权。
  4. 热爱开发、专注技术:主导并负责多个项目,常独立设计项目方案、研发关键模块等,项目之余努力学习计算机相关基础理论知识,努力做到理论、实践结合,持续提高工作效率。

个人动态

bitkylin 发布了文章 · 8月22日

Multi Module Spring Boot集成测试使用JaCoCo生成测试覆盖率

一般的SpringBoot项目会由多Module组成,每个Module为不同的功能模块。项目启动时,多个Module提供不同的服务,共同支持了本项目所提供的服务。若采用启动SpringBoot的方式进行多Module集成测试,一般test case会放在SpringApplication类所在的Module中,该Module一般仅提供了服务的入口,并无太多实际业务功能「简单来说,业务代码都不在这个Module中」。本文探讨运行集成测试,对多Module测试覆盖率合并统计的方法。

项目结构

本文所述的项目具有如下结构:

- pom.xml

- subModule 1
- - pom.xml

- subModule 2
- - pom.xml

Root pom.xml管理项目公共Maven配置,subModule 1为应用入口,SpringApplication类也在其中,SpringBoot也在此Module中启动;subModule 2为业务功能模块,提供Service服务。

选择JaCoCo

一般测试覆盖率统计工具,单独在Module内执行test case,并分别为每个Module创建覆盖率报告,没有跨Module覆盖率的内置支持,也没有多个Module的合并报告。根据这篇文章Java覆盖率工具jacoco,Cobertura可知,JaCoCo有很多优势,基本成为了目前唯一可用的工具,根据JaCoCo官方文档MavenMultiModule所述,JaCoCo 0.7.7版实现了新的Maven Goal jacoco:report-aggregate 。所以本文就基于以上资料,研究并生成Maven多Module覆盖率汇总文档。

使用JaCoCo生成测试覆盖率文档

由于官方只是强调实现了该功能,并未给出最佳实践,再次通过搜索资料,发现了这篇 StackOverflow 回复给出了最佳实践,按其中的指示操作即可:

  1. 在根 pom.xml 中添加 jacoco-maven-plugin:
<?xml version="1.0" encoding="UTF-8"?>
<project>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.jacoco</groupId>
                <artifactId>jacoco-maven-plugin</artifactId>
                <version>0.8.4</version>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <build>
        <plugins>
            <plugin>
                <groupId>org.jacoco</groupId>
                <artifactId>jacoco-maven-plugin</artifactId>
                <version>0.8.4</version>
                <executions>
                    <execution>
                        <id>prepare-agent</id>
                        <goals>
                            <goal>prepare-agent</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>
  1. 在启动SpringBoot的子Module中,添加如下构建配置即可,不要忘了使用maven-surefire-plugin插件对test case进行管理。
<project>
    <dependencies>
        <dependency>
            <groupId>org.jacoco</groupId>
            <artifactId>jacoco-maven-plugin</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.jacoco</groupId>
                <artifactId>jacoco-maven-plugin</artifactId>
                <version>0.8.4</version>
                <executions>
                    <execution>
                        <id>report-aggregate</id>
                        <phase>verify</phase>
                        <goals>
                            <goal>report-aggregate</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>
  1. 配置好后,只需要在应用根目录下「Root pom.xml所在的目录下」,执行Maven指令 mvn verify,之后在SpringBoot子Module中,打开target/site/jacoco-aggregate/index.html,即可在浏览器中查看详细的测试覆盖率报告了。覆盖率报告示例如下:

image.png

参考资料

  1. Maven中测试插件(surefire)的相关配置及常用方法
  2. Creating Multi-project Builds
  3. java - Reporting and Merging multi-module jacoco reports with report-aggregate - Stack Overflow
  4. MavenMultiModule · jacoco/jacoco Wiki · GitHub
  5. Java覆盖率工具jacoco,Cobertura_zzhongcy的专栏-CSDN博客_cobertura 端口占用
查看原文

赞 0 收藏 0 评论 0

bitkylin 发布了文章 · 8月9日

基于 Hexo + NexT + GitHub 的静态博客,2020 年 8 月升级过程记录

早在 2017 年,我基于 Hexo + NexT 搭建了 GitHub 托管的静态博客。到现在快 3 年了,发生了很多变化,比如 Hexo 脚手架升级了 2 个大版本「目前最新 5.0 版」,Node.js 也升级了多个大版本,静态博客的功能进行了很多增强,为了赶上时代的潮流,故需要对之前搭建的静态博客底层框架进行全方面的升级。本文记载了完整的升级过程。本文演示在 Mac 系统下的操作过程。

首先展示升级后,主页最终效果:

image

本次升级方案如下:

  1. 使用最新版脚手架工具创建新的静态博客项目
  2. 将文章、Next 主题、Hexo 配置等迁移至新版项目中

使用该方案,可确保不会出现新老版本的兼容性问题,在稳定性、效率之间找到最好的平衡点。

工具准备

根据 Hexo 官网的介绍,进行工具的准备,首先确保系统中已安装如下工具:

  1. node.js「演示版本13.12.0」
  2. git

而后在终端依次执行如下 bash 指令:

# 国内执行 cnpm 命令更快更方便
npm install hexo-cli -g
hexo init blog
cd blog
npm install
hexo server

初始静态博客项目已经可以在本地运行起来了,接下来集成最新版 NexT 主题,根据 NexT 官网的指导,直接执行如下指令:

git clone https://github.com/theme-next/hexo-theme-next themes/next

打开 Hexo 配置文件,更改主题:

theme: next

安装 git 部署插件

npm install hexo-deployer-git --save

接下来可以开始准备数据的迁移工作了。

配置及文章迁移

只需要进行如下两步,即可完成迁移工作:

  1. 将 Hexo 目录下的 _config.yml 配置,以及主题目录下的 _config.yml 配置迁移至新的项目中
  2. 将 Hexo 目录下的 source 目录整体迁移至新的项目中

新版功能变更

  1. 在新版 NexT 主题的配置文件中,新增了很多基于 jsdelivr.net 的可选 CDN 服务,打开后可以保证三方资源文件的极快加载,建议打开。
  2. Hexo 的官方文档中,介绍了文章中插入图片的三种方法,不过无论是哪种方法,在使用本地 Markdown 编辑器进行文章编写时,都无法预览图片。为了解决此问题,可以安装一个图片路径转换的插件 hexo-asset-image,但是该插件目前处于废弃状态。目前未找到好的解决方法,暂时实用官网推荐的「相对路径的标签引用」。
  3. icon 资源的引用格式出现了变化,详情可以参考配置示例,需要注意变更。

感受

虽然 Hexo + NexT 整体升级了 1 到 2 个大版本,细节功能明显增加了很多「从配置的规模就可以看出来」,页面细节更加现代化,但是主体功能并无变化。首次迁移后,出现部分图片、图标找不到的情况,后续通过研究后都得到了解决,并无其他问题,体验相比原来达到了 105% 的水平,推荐升级。

参考链接

  1. Hexo 中完美插入本地图片
  2. Hexo 脚手架官网
  3. NexT 主题官网
查看原文

赞 1 收藏 1 评论 0

bitkylin 发布了文章 · 7月26日

「原理分析」Spring Boot启动时基于spring.factories自动读取远端Environment实现的原理源码分析

采用Spring标准的事件/监听器模型,通过Spring SPI的方式,在Spring Boot启动时,自动读取远端「远程服务器、本地硬盘等」Environment配置,方便在Spring Boot启动前,对配置进行灵活调整,增加灵活性,减少硬编码。

本文先从原理进行分析,表明其可行性,下一篇文章再展示具体的代码实现。首先从SPI的基础开始讲起。

1. 服务发现的基础:SPI

注:此小节内容描述主要参考此文章 spring.factories
在Spring Boot中有一种非常解耦的扩展机制:Spring Factories。这种扩展机制实际上是仿照Java中的SPI扩展机制来实现的。

1.1 背景描述

系统中各个模块,往往有很多不同的实现方案,如日志组件、JDBC驱动、XML解析组件等。面向对象的程序设计中,推荐使用面向接口编程,业务程序中如需使用某项功能,须依赖通用的标准接口。基于可拔插的设计原则,此时如需更换功能模块的底层实现,直接予以替换即可「如替换Jar、替换maven依赖等」,业务代码无需任何改动。上述想法很美好,但是程序使用依赖的功能模块时,必须进行指明,不然程序运行时可能找不到相应的实现类,但是为了解耦,我们不想在业务代码中声明具体的实现,有什么解决方法吗?

这就需要一种服务发现机制。Java SPI就是提供这样的一个机制。Java SPI机制「Service Provider Interface」主要用于插件等,如需详细了解可参考java.util.ServiceLoader的文档。

1.2 Java SPI约定

Java SPI的具体约定为:当服务的提供者,提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。 基于这样一个约定就能很好的找到服务接口的实现类,而不需要再在代码里指定。JDK中提供了服务实现查找的一个工具类:java.util.ServiceLoader

1.3 Spring Boot中的SPI机制

在Spring中也有一种类似与Java SPI的加载机制。它在META-INF/spring.factories文件中配置接口的实现类名称,然后在程序中读取这些配置文件并实例化。这种自定义的SPI机制是Spring Boot Starter实现的基础。

2. Spring Boot实现SPI的源码分析

下面就根据Spring Boot应用的启动过程,对源码进行简要分析。当然Spring Boot本质是对Spring的再封装,故以下内容适用于Spring,只是部分源码是Spring Boot专属的。要注意的是,为了节省篇幅,避免喧宾夺主,会对实际源码进行精简,以突出要表述的内容。

首先展示最经典的Spring Boot启动代码,本节从此处讲起,如下:

public class Application {
    public static void main(String[] args) {
        SpringApplication application = new SpringApplication(Application.class);
        application.run(args);
    }
}

2.1 实例化SpringApplication对象

在实例化SpringApplication对象时,可以看到程序调用了如下构造方法。在执行到setInitializers((Collection)getSpringFactoriesInstances(ApplicationContextInitializer.class));时,即触发了Spring实现的SPI。

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
    this.resourceLoader = resourceLoader;
    Assert.notNull(primarySources, "PrimarySources must not be null");
    this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
    this.webApplicationType = WebApplicationType.deduceFromClasspath();
    setInitializers((Collection) getSpringFactoriesInstances(
            ApplicationContextInitializer.class));
    setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
    this.mainApplicationClass = deduceMainApplicationClass();
}

继续深入看该方法的具体实现,定位到该方法:org.springframework.boot.SpringApplication#getSpringFactoriesInstances,该方法的源码如下:

private <T> Collection<T> getSpringFactoriesInstances(Class<T> type,
        Class<?>[] parameterTypes, Object... args) {
    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
    Set<String> names = new LinkedHashSet<>(
            SpringFactoriesLoader.loadFactoryNames(type, classLoader));
    List<T> instances = createSpringFactoriesInstances(type, parameterTypes,
            classLoader, args, names);
    AnnotationAwareOrderComparator.sort(instances);
    return instances;
}

Spring-Core包里定义了SpringFactoriesLoader类,该类实现了检索META-INF/spring.factories文件,并获取指定接口的配置的功能。在这个类中定义了两个对外的方法:
loadFactories:根据接口类获取其实现类的实例,这个方法返回的是对象列表。
loadFactoryNames:根据接口获取其接口类的名称,这个方法返回的是类名的列表。
上面的两个方法的关键都是从指定的ClassLoader中获取spring.factories文件,并解析得到类名列表,

此处使用的是loadFactoryNames方法。继续深入发现实际调用的是loadSpringFactories方法:

private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
    MultiValueMap<String, String> result = cache.get(classLoader);
    if (result != null) {
        return result;
    }
    try {
        Enumeration<URL> urls = (classLoader != null ?
                classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
                ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
        result = new LinkedMultiValueMap<>();
        while (urls.hasMoreElements()) {
            URL url = urls.nextElement();
            UrlResource resource = new UrlResource(url);
            Properties properties = PropertiesLoaderUtils.loadProperties(resource);
            for (Map.Entry<?, ?> entry : properties.entrySet()) {
                List<String> factoryClassNames = Arrays.asList(
                        StringUtils.commaDelimitedListToStringArray((String) entry.getValue()));
                result.addAll((String) entry.getKey(), factoryClassNames);
            }
        }
        cache.put(classLoader, result);
        return result;
    }
    catch (IOException ex) {
        throw new IllegalArgumentException("Unable to load factories from location [" +
                FACTORIES_RESOURCE_LOCATION + "]", ex);
    }
}

其中静态常量FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories"

2.2 加载factories文件

java.lang.ClassLoader#getResources方法会遍历整个项目「包含依赖」的META-INF/spring.factories文件,取得其绝对路径,如/Users/bitkylin/.m2/repository/cn/bitkylin/test/1.0.0/test.jar/META-INF/spring.factories,使用PropertiesLoaderUtils#loadProperties方法从路径加载,并最终将接口和其实现类的全名缓存在cache对象中。cache对象的结构如下:

loadProperties读取.png

一颗多叉树。将spring.factories中配置的所有接口和其实现类的全名都读取了出来。此接口将接口org.springframework.context.ApplicationListener的实现类的类名的集合作为结果返回,而后org.springframework.boot.SpringApplication#createSpringFactoriesInstances方法将上述实现类均进行实例化,此时监听器就都创建好并注册了。

spring.factories是通过Properties解析得到的,我们可以按照如下规则编写:

com.xxx.interface=com.xxx.classname

key是接口,value是实现类。系统会自动将其初始化为如图所示的结构,方便使用。

2.3 Spring Boot启动

调用org.springframework.boot.SpringApplication#run方法,开始启动Spring Boot。在启动最开始阶段,程序就会调用到org.springframework.boot.SpringApplication#prepareEnvironment方法,并最终调用到经典的org.springframework.context.event.SimpleApplicationEventMulticaster#invokeListener方法「典型的观察者模式,标准的Spring事件/监听器模型」,源码如下:

protected void invokeListener(ApplicationListener<?> listener, ApplicationEvent event) {
    ErrorHandler errorHandler = getErrorHandler();
    if (errorHandler != null) {
        try {
            doInvokeListener(listener, event);
        }
        catch (Throwable err) {
            errorHandler.handleError(err);
        }
    }
    else {
        doInvokeListener(listener, event);
    }
}

通过该方法,将事件ApplicationEnvironmentPreparedEvent传递到所有已注册的监听器,可以借此实现Spring Boot启动时自动读取远端Environment。具体做法下节再讲述。

参考链接

spring.factories

查看原文

赞 0 收藏 0 评论 0

bitkylin 发布了文章 · 2018-06-14

Ubuntu 下 Oh My Zsh 的最佳实践「安装及配置」

Oh My Zsh 是一款社区驱动的命令行工具,是基于 Zsh 命令行的一个扩展工具集,提供了丰富的扩展功能,如:主题配置,插件机制,内置的便捷操作等,可以给我们一种全新的命令行使用体验。下文对 Oh My Zsh 的安装及配置方法进行总结,只总结最佳的实践。

1. 安装 Oh My Zsh

第一步:安装 Zsh

# 安装 Zsh
sudo apt install zsh

# 将 Zsh 设置为默认 Shell
chsh -s /bin/zsh

# 可以通过 echo $SHELL 查看当前默认的 Shell,如果没有改为 /bin/zsh,那么需要重启 Shell。

第二步:安装 Oh My Zsh

# 安装 Oh My Zsh
wget https://github.com/robbyrussell/oh-my-zsh/raw/master/tools/install.sh -O - | sh
# 以上命令可能不好使,可使用如下两条命令
wget https://github.com/robbyrussell/oh-my-zsh/raw/master/tools/install.sh
bash ./install.sh

2. Zsh 的配置

2.1 字体的安装

推荐在终端使用 Powerline 类型的主题,该类型主题可以使用图形表示尽可能多的信息,方便用户的使用。推荐安装用户量最大的 Powerlevel9k

Powerlevel9k 中需要使用较多的图形符号,字体大多不会自带这些符号,所以需要使用专门的 Powerline 字体。

不推荐安装官方默认的 Powerline Fonts,理由是图形符号不全,符号处会有乱码。推荐安装 Nerd-Fonts 系列字体,因为该系列字体附带有尽可能全的符号,并且更新非常频繁,项目地址在这里。例如直接下载 Ubuntu Font Family 中的 Ubuntu Nerd Font Complete.ttf ,然后直接在Ubuntu下安装。

2.2 主题及字体的配置

如果要在  Oh My Zsh中安装 Powerlevel9k ,只需执行如下指令:

git clone https://github.com/bhilburn/powerlevel9k.git ~/.oh-my-zsh/custom/themes/powerlevel9k

3. 插件配置

3.1 autojump

更快地切换目录,不受当前所在目录的限制。

安装:

sudo apt install autojump

用法:

# 跳转到目录
j dir
# 可以通过GUI文件管理器打开指定目录,执行命令:
jo dir

3.2 fasd

快速访问文件或目录,功能比前一个插件强大。

安装:

sudo apt install fasd

用法:

alias f='fasd -f'          # 文件
alias d='fasd -d'        # 目录
alias a='fasd -a'        # 任意
alias s='fasd -si'       # 显示并选择

alias sd='fasd -sid'        # 选择目录
alias sf='fasd -sif'          # 选择文件
alias z='fasd_cd -d'       # 跳转至目录
alias zz='fasd_cd -d -i'  # 选择并跳转至目录

3.3 zsh-autosuggestions

命令行命令键入时的历史命令建议插件

按照官方文档提示,直接执行如下命令安装:

git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions

3.4 zsh-syntax-highlighting

命令行语法高亮插件

按照官方文档提示,直接执行如下命令安装:

 git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting

3.5 插件最终配置

# autojump 功能弱,fasd 功能强,但是没 autojump 实用
# 值得注意的是,根据官方文档,zsh-syntax-highlighting 插件需放在最后
plugins=(
  git extract autojump zsh-autosuggestions zsh-syntax-highlighting
)

4.「.zshrc」文件完整修改

Oh My Zsh 配置文件的完整修改结果,只有对配置文件进行如下修改,才能使上述配置生效。

# 设置字体模式以及配置命令行的主题,语句顺序不能颠倒
POWERLEVEL9K_MODE='nerdfont-complete'
ZSH_THEME="powerlevel9k/powerlevel9k"

# 以下内容去掉注释即可生效:
# 启动错误命令自动更正
ENABLE_CORRECTION="true"

# 在命令执行的过程中,使用小红点进行提示
COMPLETION_WAITING_DOTS="true"

# 启用已安装的插件
plugins=(
  git extract fasd zsh-autosuggestions zsh-syntax-highlighting
)

常用命令

下面总结 Oh My Zsh 配置相关的其他 bash 命令:

#  查看当前所用的 Shell
echo $SHELL

# 查看系统内已安装的 Shell
cat /etc/shells

# 用 GUI 文件管理器或编辑器打开指定的的文件或目录
xdg-open fileOrDir

参考资料

  1. Zsh + Oh My Zsh 全程指南「程序员必备」
  2. Zsh 全程指南
  3. Ubuntu 16.04 下安装 Zsh 和 Oh My Zsh
查看原文

赞 6 收藏 4 评论 0

bitkylin 发布了文章 · 2018-06-05

Spring Boot + MongoDB 应用的 Docker 化实践

本文旨在通过将一个具体的 Spring Boot + MongoDB 项目进行 Docker 化处理,从而对 Docker 的基本用法进行一次实践。该项目 Docker 化后,后端服务访问数据库正常,仍然可以打开后端服务托管的单页 Web 应用,总之项目运行状态符合预期。具体使用的项目见 这个 GitHub 仓库

学习 Docker 的基本原理、基本用法可以参考这本开源电子书《Docker — 从入门到实践》,我觉得这本书写的算是目前最好的了,至少比一些同类出版书刊好。

Docker 的基本原理、基本用法均不在这篇文章介绍,想学习 Docker 请参考上面这本开源书,本文进记录一次项目的 Docker 化实践。

1. 环境准备

  • java: JDK 1.8
  • Linux: Ubuntu 17.10.1
  • Docker CE: 17.12.0

2. 官方 Docker 镜像的获取

在 Docker Store 中,搜索想要获取的 Docker 镜像,然后按照说明获取即可。

从 Docker Store 中获取官方镜像

2.1 获取 Java JRE

docker pull store/oracle/serverjre:8 

2.2 获取 Java JRE

docker pull mongo 

3. 构建自定义 Docker 容器

Java 项目构建为 Docker 容器需要如下文件:

  • Jar 文件
  • 本地配置文件
  • Dockerfile 文件

Dockerfile 的内容如下所示:

# 基础 JRE 镜像
FROM store/oracle/serverjre:8

# 修改时区,使得容器时间为北京时间
RUN echo "Asia/Shanghai" > /etc/timezone

# 将所需项目文件复制进入容器中
WORKDIR /app
ADD ./ClusterDevicePlatform-server-1.8.9-release.jar /app
ADD ./setting /app

# 暴露容器的端口
EXPOSE 30232 8080

# 运行项目
CMD ["java", "-jar", "-Dfile.encoding=UTF-8", "ClusterDevicePlatform-server-1.8.9-release.jar"]

Dockerfile 文件编写完成后,该 Java 服务器目录下会有三个文件,其中 setting 文件和 Jar 包为原始项目的组成部分,需配套使用,setting 必须放在 Jar 包的同级目录下,示意如下:

Java 服务器目录

之后执行如下命令:

docker build -t ky-server .

即可开始构建 Java 服务端应用的 Docker 镜像,生成的镜像命名为「ky-server」。

Java 服务端应用的 Docker 镜像构建完成后,执行 docker images 命令,即可获取本机保存的所有 Docker 镜像,如下所示:

本机的四个 Docker 镜像

4. 运行已容器化的 Java 服务端应用

4.1 创建 Docker Network

随着 Docker 网络的完善,官方建议将容器加入自定义的 Docker 网络来连接多个容器,下面先创建一个新的 Docker 网络,并命名为 bitky

docker network create -d bridge bitky

打开两个 Shell,分别执行如下两条指令:

docker run --rm --name kylinked-mongo  -p 27017:27017 --network bitky mongo:latest

docker run --rm --name kylinked-server -p 80:8080 -p 30232:30232 --network bitky ky-server:latest

即可分别打开在两个 Shell 前台打开 MongoDB 和 Java 服务端应用,其中 MongoDB 容器的 27017 映射到了操作系统的同名端口,Java 服务器的 8080 端口映射到了操作系统的 80 端口,方便浏览器访问。MongoDB 镜像直接通过常规方式 pull 即可,本文不再赘述。

打开一个新的终端查看容器的状态信息:

docker container ls

从图中可以直观看到端口映射状态及容器运行状态。

Docker 容器运行状态

4.2 Docker 中设置时区为东八区「北京时间」

容器中的默认时区是 UTC,如果要修改自制 Docker 镜像的时区,只需要在 Dockerfile 中加入下面两句就可以了:

RUN echo "Asia/Shanghai" > /etc/timezone
RUN dpkg-reconfigure -f noninteractive tzdata

需要注意的是,本文给出的 Dockerfile 已添加了如上的语句,不需要重复添加该语句。 RUN dpkg-reconfigure -f noninteractive tzdata 是 Ubuntu 修改时区的命令。如果你的自定义镜像使用的是其他发行版,那么这里的命令也要改变。本文采用的是 JRE 作为基础镜像,经过实验发现,只需要执行指令的第一句即可。

参考链接

  1. Docker Documentation
  2. Docker 部署 SpringBoot 项目整合 Redis 镜像做访问计数 Demo
  3. Spring Boot 应用发布到 Docker
  4. 一步步带你构建 Spring Boot + Docker 应用
  5. Docker 中如何设置 container 的时区
查看原文

赞 2 收藏 2 评论 0

bitkylin 发布了文章 · 2018-04-27

基于 Netty 的可插拔业务通信协议的实现「3」业务注册及实际工作流程

本文为该系列的第三篇文章,设计需求为:服务端程序和众多客户端程序通过 TCP 协议进行通信,通信双方需通信的消息种类众多。上一篇文章以一个具体的需求为例,探讨了指定的 Java 消息对象与其相应的二进制数据帧相互转换的方法。本文仍以该实例为例,探讨该自定义通信协议的具体工作流程,以及如何以注册的形式灵活插拔通信消息对象。

1. 以注册的形式实现通信消息对象的统一管理

通过该系列的第二篇文章可知,各个消息对象的编解码器类均拥有一个静态工厂方法,用于手动传入功能位及功能文字描述,进而生成包含这些参数的编解码器。如此设计,使得所有消息的功能位和文字描述均能够统一管理,降低维护成本。

根据上述需求,可通过 Map 容器管理所有的编解码器,有如下优点:

  1. 进行消息对象生成操作时,可直接使用相应编解码器的消息对象静态创建方法。
  2. 进行消息对象的编码操作时,已拥有该 Java 消息对象,即可知道消息对象的功能位,据此可获取相应的编解码器;或者,每个 Java 消息对象均内含相应编解码器的引用,故可直接对该消息对象进行编码操作。
  3. 进行二进制数据帧的解码操作时,数据帧中已包含了消息的功能位,据此可获取相应的编解码器,而后可以对该数据帧进行解析,生成相应的 Java 消息对象。

通信消息对象注册方法如下所示:

/**
 * 消息对象的注册
 *
 * @param toolkit 消息对象编解码器容器的工具类
 */
private void initialMsg() {
    saveNormalMsgCodec(new MsgCodecDeviceUnlock(0x10, 0x11, "客户端解锁"));
    saveNormalMsgCodec(new MsgCodecDeviceClear(0x10, 0x13, "客户端初始化"));
    saveNormalMsgCodec(new MsgCodecDeviceId(0x10, 0x1B, "客户端ID设置"));
    saveNormalMsgCodec(new MsgCodecEmployeeName(0x10, 0x1C, "客户端别名设置"));
    ... ...
}

/**
 * 将普通消息对象及其回复消息对象的编解码器均保存到 HashMap 中
 *
 * @param baseMsgCodec 特定的消息对象编解码器
 */
private void saveNormalMsgCodec(BaseMsgCodec baseMsgCodec) {
    saveSpecialMsgCodec(baseMsgCodec);
    baseMsgCodec = new MsgCodecReplyNormal(baseMsgCodec.getMajorMsgId() + 0x10, baseMsgCodec.getSubMsgId(), baseMsgCodec.getDetail());
    saveSpecialMsgCodec(baseMsgCodec);
}

/**
 * 将消息对象的编解码器保存到 HashMap 中
 *
 * @param baseMsgCodec 特定的编解码器
 */
private void saveSpecialMsgCodec(BaseMsgCodec baseMsgCodec) {
    HASH_MAP.put(figureFrameId(baseMsgCodec.getMajorMsgId(), baseMsgCodec.getSubMsgId()), baseMsgCodec);
}

上述代码表明,如果有新的业务需求,需要增删「插拔」业务消息对象,只需在 initialMsg() 方法中,对相应编解码器的注册语句进行增删即可。

saveNormalMsgCodec(BaseMsgCodec) 方法可以同时注册特定业务消息对象及其通用回复消息对象,操作方法清晰、简洁。

所以,在启动该 Java 程序时,只需要在启动过程中,执行上述 initialMsg() 方法,即可完成所有业务消息对象的注册。

2. 多个消息对象自由组合进同一个数据帧的实现原理

由该系列的第一篇文章可知,如果某二进制数据帧所要传输的数据体部分内容很少,导致一个帧的大部分容量均被帧头占据,导致有效数据的占比很小,这就产生了巨大的浪费,故数据帧的数据体部分由子帧组成,同一类子帧均可被组装进同一个数据帧。如此做法,整个通信链路的数据量会明显减少,IO 负担也会因此减轻。

该需求的实现原理如下所示:

/**
 * 启动一个Channel的定时任务,用于间隔指定的时间对消息队列进行轮询,并发送指定数据帧
 *
 * @param deque     指定的消息发送队列
 * @param channelId 指定 Channel 的序号
 */
private void startMessageQueueTask(LinkedBlockingDeque<BaseMsg> deque, Integer channelId) {
    executorService.scheduleWithFixedDelay(() -> {
        try {
            BaseMsg baseMsg = deque.take();         // 从队列中取出一个消息对象,队列为空时阻塞
            Thread.sleep(AWAKE_TO_PROCESS_INTERVAL);// 等待极短的时间,保证队列中缓存尽可能多的对象
            Channel channel = touchChannel(channelId); // 获取指定的待发送的 Channel
            List<ByteBuf> dataList = new ArrayList<>();// 子帧容器
            ByteBuf data = baseMsg.subFrameEncode(channel.alloc().buffer());// 编码一个子帧
            dataList.add(data);
            touchNeedReplyMsg(baseMsg);            // 对该子帧设置检错重发任务
            int length = data.readableBytes();
            int flag = baseMsg.combineFrameFlag(); // 获取消息对象标识
            while (true) {
                BaseMsg subMsg = deque.peek();     // 查看队列中的第一个消息对象
                if (subMsg == null || subMsg.combineFrameFlag() != flag) {
                    break; // 消息对象标识不同,即欲生成的主帧帧头不同,不能组合进同一主帧
                }
                data = subMsg.subFrameEncode(channel.alloc().buffer());
                if (length + data.readableBytes() > FrameSetting.MAX_DATA_LENGTH) {
                    break;
                }
                length += data.readableBytes();
                dataList.add(data);                // 组合进了同一主帧
                deque.poll();                      // 从队列中移除该消息对象
                touchNeedReplyMsg(subMsg);
            }
            FrameMajorHeader frameHeader = new FrameMajorHeader(
                    baseMsg.getMajorMsgId(),
                    baseMsg.getGroupId(),
                    baseMsg.getDeviceId(),
                    length);                       // 生成主帧帧头消息对象
            channel.writeAndFlush(new SendableMsgContainer(frameHeader, dataList)); // 送入Channel进行发送
        } catch (InterruptedException e) {
            logger.warn("消息队列定时发送任务被中断");
        }
    }, channelId, CommSetting.FRAME_SEND_INTERVAL, TimeUnit.MILLISECONDS);
}

由代码可知,待发送的消息对象均被送入指定的发送队列进行缓存,某客户端相应的线程对队列进行操作,取出消息对象并进行编码、组装、发送等。当然,当客户端数量较多时,上述的线程实现方式可采用 Netty 的 NIO 方式进行优化,以降低系统开销。

由上述描述可知,欲发送一个消息对象,只需将该消息对象送入相应的发送队列即可。

3. 实际业务消息对象的编解码

3.1 消息对象的编码方式

由于每个 Java 消息对象均内含相应编解码器的引用,故可直接对该消息对象进行编码操作,代码如下:

public abstract class BaseMsg implements Cloneable {
    private final BaseMsgCodec msgCodec;
    ... ...

    /**
     * 将 java 消息对象编码为 TCP 子帧
     *
     * @param buffer 空白的 TCP 子帧的容器
     * @return 保存有 TCP 子帧的容器
     */
    public ByteBuf subFrameEncode(ByteBuf buffer) {
        return msgCodec.code(this, buffer);
    }
}

3.2 消息对象的解码方式

首先根据数据帧的帧头,即可解析出 FrameMajorHeader 对象,然后即可调用如下方法完成子帧的解析工作。实现原理文章开头已指出。

/**
 * TCP 帧解码为 Java 消息对象
 *
 * @param head     主帧头
 * @param subMsgId 子帧功能位
 * @param data     子帧数据
 * @return 已解码的 Java 对象
 */
public BaseMsg decode(FrameMajorHeader head, int subMsgId, byte[] data) {
    BaseMsgCodec msgCodec = MsgCodecToolkit.getMsgCodec(head.getMsgId(), subMsgId);
    return msgCodec.decode(head.getGroupId(), head.getDeviceId(), data);
}
查看原文

赞 2 收藏 1 评论 0

bitkylin 发布了文章 · 2018-03-29

基于 Netty 的可插拔业务通信协议的实现「2」特定业务消息对象的设计

本文为该系列的第二篇文章,设计需求为:服务端程序和众多客户端程序通过 TCP 协议进行通信,通信双方需通信的消息种类众多。上一篇文章详细描述了该通信协议的二进制数据帧格式以及基本 Java 消息类,假设通信双方「服务端、客户端」均由 Netty 框架构建而成,双方在程序内部使用 Java 消息对象,通信双方信息交互采用的是自定义二进制帧格式,本文通过一个具体实例,探讨指定的 Java 消息对象与其相应的二进制数据帧相互转换的方法。

1 特定 Java 消息对象通信举例

本小节以一个具体的需求为例,讲述该自定义通信协议的工作流程。该需求为:对某一个特定的客户端进行命名。该需求的具体工作流程描述如下:

服务端需主动向指定的客户端发送消息,对客户端设置指定的名称,客户端接到指定的消息并验证合法后,需向服务端反馈消息接受成功的确认回复,服务器接收到该回复后,即可认为对客户端进行命名的消息发送成功并且名字设置成功,若服务端在指定的时间内未收到回复,需进行重发或者向上层「如管理员或数据库」反馈该客户端的异常。

上述过程使用 UML 序列图演示如下:

UML 序列图演示客户端别名的设置

由上图可以直观地看出:管理员对服务器的操作以及服务器对管理员的反馈均为动作,Server 与 Client 之间的通信以 Java 的视角均通过 Java 消息对象,共需两个对象:客户端别名设置对象、客户端别名设置回复对象。而实际两者之间的通信使用的是基于 TCP 的自定义二进制数据帧,对象与数据帧之间需进行转换。

2 该任务所需 Java 消息类的设计

上小节所述过程需要两个 Java 消息类,如下所示:

  1. 客户端别名设置类
/**
 * 「消息对象」客户端别名设置
 */
public class MsgDeviceName extends BaseMsg {

    private final String name;

    public MsgDeviceName(BaseMsgCodec msgCodec, int groupId, int deviceId, String name) {
        super(msgCodec, groupId, deviceId);
        this.name = name;
    }

    public String getName() {
        return name;
    }

    @Override
    public String msgDetailToString() {
        return super.msgDetailToString() + "别名:" + name;
    }
}
  1. 客户端别名设置回复类「直接使用通用回复类」
/**
 * 「消息对象」通用消息回复
 */
public class MsgReplyNormal extends BaseMsg {

    public MsgReplyNormal(BaseMsgCodec msgCodec, int groupId, int deviceId) {
        super(msgCodec, groupId, deviceId);
    }

    @Override
    public String msgDetailToString() {
        return super.msgDetailToString();
    }
}

客户端别名设置类相比于基础消息类,覆写了消息细节描述方法,优化调试日志的使用体验。主要改变是,仅仅增加了客户端别名的引用及其 Get 方法;而对于客户端别名设置回复,直接使用了通用回复类,减小了设计的复杂度。

该自定义帧协议有一个设计要点:每一个功能性消息类均有相对应的特定回复类。从功能位的角度来看,该两种类的主帧功能位之间存在如下关系:

消息回复类功能位 - 消息类功能位 = 0x10

即两类的功能位数值之差以十六进制表示为 0x10。据此设计功能性 Java 消息类后,不需要专门设计对应的回复类,系统会自行使用该通用回复类进行工作。

3 该任务所需消息类编解码器的设计

编码器可将 Java 消息对象编码为数据帧,解码器可讲数据帧解码为指定的 Java 消息对象,上节所述的两种消息类均需要相对应的编解码器,如下所示:

3.1 客户端别名设置编解码器类

该类相比于基础类,新增了编解码器的静态工厂方法,手动传入功能位及功能文字描述,进而生成包含这些参数的编解码器。如此设计,使得所有消息的功能位和文字描述均能够统一管理,降低维护成本。

该类实现了编码、解码方法,故可对消息对象进行编码或对数据帧进行解码。该类的实现如下所示:

/**
 * 「消息对象编解码器」客户端别名设置
 */
public class MsgCodecDeviceName extends BaseMsgCodec {

    private static MsgCodecDeviceName msgCodec = null;

    public MsgCodecDeviceName (int majorMsgId, int subMsgId, String detail) {
        super(majorMsgId, subMsgId, detail);
        msgCodec = this;
    }

    public static MsgDeviceName create(int groupId, int deviceId, String name) {
        return new MsgDeviceName(msgCodec, groupId, deviceId, name);
    }

    @Override
    public ByteBuf code(BaseMsg msg, ByteBuf buffer) {
        MsgDeviceName message = (MsgDeviceName) msg;
        buffer.writeByte(message.getSubMsgId());
        byte[] data = KyToArrayUtil.stringToArray(message.getName());
        buffer.writeShort(data.length);
        buffer.writeBytes(data);
        return buffer;
    }

    @Override
    public MsgDeviceName decode(int groupId, int deviceId, byte[] data) {
        String name = KyToArrayUtil.arrayToString(data);
        return create(groupId, deviceId, name);
    }
}

3.2 通用回复编解码器类

该类相比于基础类,新增了编解码器的静态工厂方法,实现了编解码器,理由与上小节相同。该类的 createByBaseMsg(BaseMsg) 静态方法可通过指定功能消息对象生成相应的回复对象。该类的实现如下所示:

/**
 * 「消息对象编解码器」通用消息回复
 */
public class MsgCodecReplyNormal extends BaseMsgCodec {

    private static MsgCodecReplyNormal msgCodec = null;

    public MsgCodecReplyNormal(int majorMsgId, int subMsgId, String detail) {
        super(majorMsgId, subMsgId, detail);
        msgCodec = this;
    }

    /**
     * 根据收到的消息对象,创建新的通用消息回复对象,
     *
     * @param msg 收到的消息对象
     * @return 新的通用消息回复对象
     */
    public static MsgReplyNormal createByBaseMsg(BaseMsg msg) {
        BaseMsgCodec msgCodec = MsgCodecToolkit.getMsgCodec(msg.getMajorMsgId() + 0x10, msg.getSubMsgId());
        if (msgCodec == null) {
            return null;
        }
        return new MsgReplyNormal(msgCodec, msg.getGroupId(), msg.getDeviceId());
    }

    /**
     * 创建新的通用消息回复对象
     *
     * @param groupId  组号
     * @param deviceId 设备号
     * @return 生成的通用消息回复对象
     */
    private MsgReplyNormal create(int groupId, int deviceId) {
        return new MsgReplyNormal(this, groupId, deviceId);
    }

    @Override
    public ByteBuf code(BaseMsg msg, ByteBuf buffer) {
        MsgReplyNormal message = (MsgReplyNormal) msg;
        buffer.writeByte(message.getSubMsgId());
        buffer.writeShort(0);
        return buffer;
    }

    @Override
    public MsgReplyNormal decode(int groupId, int deviceId, byte[] data) {
        return create(groupId, deviceId);
    }
}
查看原文

赞 0 收藏 3 评论 0

bitkylin 发布了文章 · 2018-02-28

基于 Netty 的可插拔业务通信协议的实现「1」协议描述及基本消息对象设计

开发工程中,有一个常见的需求:服务端程序和多个客户端程序通过 TCP 协议进行通信,通信双方需通信的消息种类众多,并且客户端的数量可能有数万个。为此,双方需要约定尽可能丰富、灵活的数据帧「数据包」协议,方便后续业务功能的设计。

本文设计了一种通信协议,为压缩数据量,该协议的数据帧以二进制方式进行传输并识别,即其基本单位为字节,必要时将部分字节流手动转化为可读文本。通过设定功能位来实现丰富的通信消息类型,并且采用注册的方式,可方便扩展新的业务消息类型,可灵活地增删通信消息对象。采用 Netty 框架保证高并发场景下程序的性能。

系统整体设计框图如下:

系统整体设计框图

1. 通信数据帧协议的设计

1.1 数据帧主帧的帧格式

首先给出通用的数据帧格式如下,一个数据帧主帧由:帧识别位、帧功能位、设备号、数据长度、数据体等 5 部分组成。「其实最通用的数据帧只有帧识别位,根据帧识别位确定帧类型,从而确定其余四个部分,本文中帧识别位固定,帧格式即固定了」

数据帧格式

  • 帧识别位:确定数据帧的开始,亦确定本帧的帧类型。
  • 帧功能位:确定该帧所传送的消息类型,特定的帧功能位对应特定的数据体。
  • 设备号:设备的识别号,服务端据此识别不同的客户端。
  • 数据长度:数据体所占用的字节数。
  • 数据体:根据帧功能位,所确定的需传输的具体的消息。

1.2 数据帧子帧的帧格式

数据帧除数据体以外的部分称为帧头,考虑这样一种需求,如果某帧所要传输的数据体部分内容很少,导致一个帧的大部分容量均被帧头占据,导致有效数据的占比很小,这就产生了巨大的浪费,举例如下:

  • 如一个开锁帧,只需传输一个开锁信号即可,消息的接收方、消息类型均体现在了帧头中,数据部分只需要 0 个或 1 个字节即可。
  • 客户端需要向服务器发送自己的当前状态信息,该状态信息可能也只需要 1 个字节左右。

由于如上实际的需求,如果增大了每一帧的有效数据的占比,整个通信链路的数据量会明显减少,IO 负担也会因此减轻,所以据此继续对帧协议进行设计。

数据帧中子帧格式

如上图,对数据帧主帧中的「数据体」部分进行进一步拆分,数据帧主帧的数据体部分由子帧组成,子帧由:子帧功能位、数据长度、数据体等 3 部分组成。

  • 子帧功能位:确定该子帧所传送的消息类型,总而言之,主帧、子帧功能位共同确定了该子帧的消息类型。
  • 数据长度:数据体所占用的字节数。
  • 数据体:根据子帧功能位,所确定的需传输的具体的消息。

1.3 数据帧的帧格式总览

完整的帧格式如下图所示,数据帧主帧的数据体部分完全由子帧组成,通信双方通信时,可以往一个主帧中添加多个子帧,从而可以极大提高链路的使用效率。

数据帧的帧格式总览

2 数据帧处理模块的实现

数据帧已进行了如上精心设计,将设计的数据帧通过程序实现并投入实际使用才是最终目的。

2.1 数据帧处理的基本方法

以服务端的工作为例来进行说明。服务端程序监听指定端口,客户端通过 TCP 协议向服务器发送二进制数据消息,服务端接收到二进制数据并进行处理,此处采用责任链模式,Netty 框架内建了方便的基于责任链模式的消息处理方法:

  1. 第一个处理器将捕获的数据截取为一个一个协议约定的数据帧并送入下层处理器,如果捕获的二进制数据未符合协议约定的格式,则可以直接丢弃。「此处未考虑半包、粘包等场景」
  2. 第二个处理器捕获到约定的数据帧,则着手对不同类型数据帧进行解析,解析为不同类型的 Java 消息对象,并将反序列化成功并验证成功的 Java 对象送入下层处理器。如果上述过程失败,可以认为客户端设计不合理,导致出现无效消息,直接丢弃该对象,也可以继续通知服务端或客户端该异常情况。
  3. 第三个处理器捕获到正确的 Java 消息对象,则可以直接送入上层 Java 模块进行处理,此处可根据不同的对象类型送入不同的上层处理模块,或者在此处进行其他的工作「比如消息日志记录工作等」。

2.2 基本 Java 消息对象的设计

Java 消息对象的设计主要由两部分组成:

  • 特定数据帧对应的特定 Java 消息对象。
  • 特定 Java 消息对象对应的特定的该消息对象编解码器。

以下是基本 Java 消息对象:

public abstract class BaseMsg implements Cloneable {

    private final BaseMsgCodec msgCodec;
    private int groupId;
    private int deviceId;
    private int resendTimes = 0;

    protected BaseMsg(BaseMsgCodec msgCodec, int groupId, int deviceId) {
        this.msgCodec = msgCodec;
        this.groupId = groupId;
        this.deviceId = deviceId;
    }

    /**
     * 获取该消息对象的细节描述
     *
     * @return 该消息对象的细节描述
     */
    public String msgDetailToString() {
        return msgCodec.getDetail() +
                "[majorMsgId=" + Integer.toHexString(msgCodec.getMajorMsgId()).toUpperCase() +
                ", subMsgId=" + Integer.toHexString(msgCodec.getSubMsgId()).toUpperCase() +
                ", groupId=" + groupId +
                ", deviceId=" + deviceId + ']';
    }

    /**
     * 重发该消息对象的记录信息更新
     */
    public void doResend() {
        resendTimes++;
    }
}

由上述代码可知,每个消息对象均包含该对象对应编解码器的引用,方便获取该消息对象的扩展信息,或者方便将该消息对象重新序列化为数据帧。该类包含上节数据帧主帧及子帧的所有公共信息,仅仅未包含子帧中的数据体信息,该需求由基本 Java 消息对象的子类实现。

该类由 abstract 修饰,是抽象类,无法直接实例化,具体的工作由该类的子类完成,即由具体的真正业务相关的 Java 消息对象完成。

以下为 Java 消息对象的基本编解码器:

/**
 * 单个消息对象「帧」的编解码器
 */
public abstract class BaseMsgCodec implements SubFramecoder, SubFramedecoder {

    private final int majorMsgId;
    private final int subMsgId;
    private final String detail;

    protected BaseMsgCodec(int majorMsgId, int subMsgId, String detail) {
        this.majorMsgId = majorMsgId;
        this.subMsgId = subMsgId;
        this.detail = detail;
    }

    public String getDetail() {
        return detail;
    }

    public int getMajorMsgId() {
        return majorMsgId;
    }

    public int getSubMsgId() {
        return subMsgId;
    }
}

由上述代码可知,特定 Java 消息对象的编解码器由数据帧的主帧、子帧功能位共同决定,这样确保了消息编解码器的规范,避免消息过多时的混乱。

Java 编解码器实现了如下两个接口,表明编解码器可将 Java 消息对象编码为数据帧,或将数据帧解码为指定的 Java 消息对象:

public interface SubFramecoder {
    /**
     * 将 Java 消息对象编码为数据帧
     *
     * @param msg    消息对象
     * @param buffer TCP 数据帧的容器
     * @return 生成的 TCP 数据帧的 ByteBuf
     */
    ByteBuf code(BaseMsg msg, ByteBuf buffer);
}

public interface SubFramedecoder {
    /**
     * 将数据帧解码为指定的 Java 消息对象
     *
     * @param groupId  设备组 ID
     * @param deviceId 设备 ID
     * @param data     帧数据
     * @return 特定的 Java 消息对象
     */
    BaseMsg decode(int groupId, int deviceId, byte[] data);
}

相关项目参考「GitHub 项目基础框架开源」

  1. Java & Vue.js「集群设备管理云平台『后端部分』」
  2. 基于 Vue.js 2.0 & Element 2.0 的集群设备管理云平台
查看原文

赞 0 收藏 1 评论 0

bitkylin 发布了文章 · 2018-02-22

Windows 10 用于 Linux 子系统的一键构建、打包脚本「 Node、Gradle 项目」

最近正在开发一个 Java & Vue.js 全栈项目,该项目由以下几部分组成:Java 后端服务器、基于 Vue.js 的单页应用、基于 JavaFX 的 GUI 客户端以及其他辅助工具等。

其中 Java 服务端及客户端均依赖公共的 Jar 包,基于 Vue.js 构建生成的静态文件亦须转移至后端模块的相关目录中,由后端模块附带的 Web 服务器管理。如果对项目中的某个子模块进行修改,需要对其手动编译、移动,再对父模块进行编译,操作繁琐,本文探讨通过 Windows 10 的 Linux 子系统运行 Shell 脚本简化上述操作并进行扩展。

之前的一篇文章已探讨过 Linux 子系统的使用及在该系统下,Java、Node.js、Gradle 等工具的配置,本文不再赘述。本文开篇首先优化 Linux 子系统「Windows Subsystem for Linux (WSL) 」的使用体验,

1. 使用 wsl-terminal 增强 WSL 的使用体验

wsl-terminal 是专门为 WSL 准备的终端模拟器,主体是 mintty,另外整合了一些相当友好的特性,使用起来非常方便,也是目前用户体验最好的,推荐使用。

感觉自从用上了 wsl-terminal,完全同时获得了 Windows 10 优异的界面效果和娱乐性,以及 Linux 的专业性和效率,并且两者能够完美融合在一起,完全可以取得好于 macOS 的使用体验「此处为 YY,我未深度使用过 macOS,坐等打脸」。

wsl-terminal 主要具有如下特性:

  • 优秀的兼容性(中文显示/输入、 24 位颜色、命令输出等都正常了)。
  • 体积小巧,压缩包仅 1.7 M 多,解压后不到 10 M 。
  • 配置简单, mintty 可以直接在标题栏右键配置, wsl-terminal 的配置文件也很简单。
  • 可以直接在资源管理器右键打开终端模拟器并定位到当前目录。
  • 可以将 .sh/.py/.pl 脚本关联到用 wsl-terminal 运行。
  • 可以将文本文件关联到用 wsl-terminal 里的 vim 运行。
  • 支持 tmux ,可以在 tmux 里打开新目录,恢复已有的 tmux 会话等。
  • 支持在 WSL 里直接运行 Windows 程序。

注: 本小节摘抄了 这篇博客 的部分内容。

2. 全栈项目的整体架构

项目的整体架构如下图所示:

工程整体构架图

3. 特定脚本指令及其含义

3.1 获取当前 Shell 脚本所在的绝对路径

dirname file # 获取 file 文件的相对路径
echo $0      # 获取当前执行的脚本文件名
pwd          # 显示当前工作目录

由以上命令可总结出,获取当前 Shell 脚本所在的绝对路径的命令如下:

SH_PATH=$(cd `dirname $0`; pwd)

3.2 Web 静态文件

Gradle / Spring 工程中,./src/main/resources/static 目录下可存放静态文件,在服务端程序运行时,即可获取此目录下的静态文件,所以需要将通过 Webpack 编译、构建生成的静态文件存放在该目录下。

cp -r ./dist/* $SERVER_PATH/src/main/resources/static  # 将静态文件复制到指定位置

静态文件放在服务端工程的指定位置

3.3 获取 Gradle 构建生成的目标 Jar 文件的文件名

运行 gradle build 后,即可在 $PROJECT/build/libs 中生成目标 Jar 文件,获取该文件的文件名,以备之后生成 Jar 运行脚本所用:

NAME=`ls $PROJECT/build/libs`  # 获取当前目录下唯一的文件的文件名

3.4 获取该脚本执行时的时间

各工程模块均编译、构建完毕后,需要统一存放在同一个目录中,各个时期生成的目录以时间命名进行区分。

BUILD_TIME=`date "+%Y-%m-%d_%H-%M"`  # 获取脚本执行时的时间

4. 该项目的一键构建、打包 Shell 脚本

4.1 脚本具体执行流程

  1. 清理各个工程的历史构建缓存
  2. 编译 Web 工程生成静态文件,移入后端工程相应目录。
  3. 编译公共依赖 Jar 包,移入后端工程、客户端工程相应目录。
  4. 编译后端工程、客户端工程。
  5. 后端工程、客户端工程编译后生成的 Jar 文件移入打包目录中,该目录以脚本运行时的时间作为区分。

该脚本的具体执行流程如下图所示,具体步骤如下:

脚本执行流程

4.2 一键构建、打包脚本

本文已经将脚本的重点、流程等内容进行了详细的介绍,现在贴出脚本具体内容,如下所示:

#!/bin/bash
# 集群设备管理系统工程的 web 端、模拟客户端、服务器端等的整体清理、构建、打包、发布

PROJECT_PATH=/mnt/d/project/ClusterDevicePlatform;       # 主工程所在目录
WEB_PATH=/mnt/d/project/cluster-device-platform-web;     # Web 模块所在目录
SH_PATH=$(cd `dirname $0`; pwd)                          # 脚本所在目录
BUILD_TIME=`date "+%Y-%m-%d_%H-%M"`                      # 脚本运行时间
UTIL_JAR_PATH=$PROJECT_PATH/messageUtils;                # 公共 Jar 模块所在目录
SERVER_PATH=$PROJECT_PATH/ClusterDevicePlatform-server;  # 服务器模块所在目录
CLIENT_PATH=$PROJECT_PATH/ClusterDevicePlatform-client;  # 硬件模拟客户端模块所在目录

# 项目已编译历史文件的清理
cd $UTIL_JAR_PATH;
rm -rf ./build;
cd $CLIENT_PATH;
rm -rf ./build;
cd $SERVER_PATH;
rm -rf ./build;
rm -rf ./src/main/resources/static;
cd $WEB_PATH;
rm -rf ./dist;

# Web 编译并将静态页面文件移入服务器项目中
npm run build
if [ ! $? -eq 0 ]
then echo "Web 编译出错"
    exit 1
fi
echo Web 编译完毕
mkdir $SERVER_PATH/src/main/resources/static
cp -r ./dist/* $SERVER_PATH/src/main/resources/static

# Client、Server 的编译
cd $UTIL_JAR_PATH;
gradle build

cd $CLIENT_PATH;
gradle build

cd $SERVER_PATH;
gradle build

# 组织并集中编译生成的待发布文件
mkdir -p $PROJECT_PATH/publish/release/serverRelease_$BUILD_TIME
cd $PROJECT_PATH/publish/release/serverRelease_$BUILD_TIME
cp $CLIENT_PATH/build/libs/* .
cp $SERVER_PATH/build/libs/* .
cp $SH_PATH/template/* .

# 组装 Client、Server 的运行脚本
CLIENT_NAME=`ls $CLIENT_PATH/build/libs`
SERVER_NAME=`ls $SERVER_PATH/build/libs`

echo chcp 65001 >> run-client.ps1
echo java -jar -\'Dfile.encoding\'=UTF-8 .\\$CLIENT_NAME cdg-pc 100 >> run-client.ps1
echo '$x = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")' >> run-client.ps1

echo chcp 65001 >> run-server.ps1
echo java -jar -\'Dfile.encoding\'=UTF-8 .\\$SERVER_NAME >> run-server.ps1
echo '$x = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")' >> run-server.ps1

参考链接

  1. Shell 获取当前工作目录绝对路径
  2. 淘宝 NPM 镜像
  3. 进入 WSL 环境的多种方法比较 
  4. 更好地使用 WSL 终端环境
  5. wsl-terminal 官方 GitHub 仓库
查看原文

赞 2 收藏 3 评论 0

bitkylin 发布了文章 · 2018-02-17

使用 Linux 子系统部署 Node、Gradle 项目的构建工具

最近的一个项目,由三个 Gradle「Java」工程以及一个 Node「Vue.js」工程组成。为了使用 Linux 下的各种工具提升效率,本文探讨 Linux 子系统的使用及在该系统下,Java、Node.js、Gradle 等工具的配置。并在后一篇文章中,探讨该项目的一键构建、打包脚本,从而根本上提升开发效率。

1. Windows 10 秋季创意者更新下的 Linux 子系统

我目前使用的操作系统是最新的 Windows 10 秋季创意者更新(Version 1709),打开 Microsoft Store 并搜索 Linux,然后选择自己喜欢的 Linux 发行版安装即可,我安装的是 Ubuntu。

Microsoft Store 中选择自己喜欢的 Linux 子系统

Linux 子系统安装完毕后,使用开始屏幕找到相应应用的磁贴,并点击打开;或者使用命令行,运行命令 bash,均可打开已经安装的 Linux 子系统。

打开 Ubuntu 子系统

2. 相关工具的部署要点

现在去各自的官方网站下载 JavaNode.jsGradle 这三个工具所对应的 Linux 版本。其中 Java、Node.js 拥有独立的 Linux 版本,Gradle 的 Windows、Linux 版本是一体的。

由于在启动 Ubuntu 后,Windows 10 中的环境变量 PATH 已被自动添加至 Ubuntu 中,所以这些工具的安装、以及添加环境变量有两种方法:

  1. 这些工具直接在 Windows 下解压,并添加至 Windows 系统环境变量中。工具的 Windows、Linux 版本由 Windows 统一管理,简化操作、界面友好,并且 Gradle 只需添加一个环境变量。

Windows 下环境变量的配置

  1. 工具的 Linux 版本移入 Ubuntu 中,并在其中添加 Ubuntu 环境变量。注意 Windows 的环境变量最先被检索到,为避免冲突,Windows下的 Gradle 需进入 bin 目录,删掉用于 Linux 的 Gradle 脚本。

Gradle 的两系统执行脚本在一起

由于 Linux 子系统还是有少许不同,Windows 与 Linux 结合太紧密可能会出现 Bug,就算不是操作系统的 Bug,也可能由于工具未考虑到这样的使用方式导致出现 Bug,比如对于一体化的 Gradle,我使用方法 1 时,Gradle 确实有偶发的 Bug,将 Gradle 独立开来问题得以解决。

3. 部署并配置 Java、Node.js、Gradle 的环境变量

Java、Node.js、Gradle 下载完毕后,在 bash 中指向下载目录,并将其复制到 home 目录并解压:

# 将工具从 Windows 目录复制至 home 目录中
cp jdk-8u162-linux-x64.tar.gz /home/lml/soft/
cp node-v9.5.0-linux-x64.tar.xz /home/lml/soft/
cp gradle-4.5.1-bin.zip /home/lml/soft/

cd /home/lml/soft/

# 解压缩各工具
tar -zxvf jdk-8u162-linux-x64.tar.gz jdk1.8.0_162/
tar xvJf node-v9.5.0-linux-x64.tar.xz
unzip gradle-4.5.1-bin.zip

#添加环境变量,并使设置立即生效
vi ~/.bashrc
source ~/.bashrc

上述命令修改 ~/.bashrc 文件时,在该文件末尾新增如下脚本内容:

NODE_HOME=/home/lml/soft/node-v9.5.0-linux-x64
JAVA_HOME=/home/lml/soft/jdk1.8.0_162
GRADLE_HOME=/home/lml/soft/gradle-4.5.1

export NODE_HOME
export JAVA_HOME
export GRADLE_HOME

PATH=$PATH:$JAVA_HOME/bin:$NODE_HOME/bin:$GRADLE_HOME/bin
export PATH

脚本生效并执行后,在 bash 中可以看到上述三个工具均可识别成功:

查看工具的配置结果

从上图可以看出,Windows 环境变量也在其中,也可通过在 /usr/bin 目录下添加软链接的方式,识别相关命令:

添加 Java 的软链接

4. 使用 SDKMAN! 管理各开发工具

在参考 Gradle 的官方文档时,官方推荐使用 SDKMAN! 安装、管理 Gradle,SDKMAN! 用于多种版本开发工具的切换、安装和卸载的工作,包括 Gradle、Java 等 Jvm 系的工具。由于未科学的上网,该工具速度不佳,不过通过文档能看到该工具简单易用。

参考链接

  1. Windows 10 内置 Linux 子系统初体验
  2. Ubuntu 设置环境变量并立即生效
  3. 设置 Linux 环境变量的方法和区别 (Ubuntu)
  4. SDKMAN! 官网
  5. SDKMAN! 工具的使用
  6. SDKMAN! 简明安装教程
查看原文

赞 1 收藏 0 评论 0

认证与成就

  • 获得 27 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

  • 集群设备管理云平台

    Java & Vue.js 全栈「集群设备管理云平台『后端部分』」,使用 Spring、Netty 搭建 TCP 服务器与上万设备的集群通信,基于 JavaFX 的 GUI 应用程序模拟上万台设备的行为,并可对服务器进行压力测试。

注册于 2017-02-10
个人主页被 546 人浏览