漫漫征途:筑梦于职场风雨,吟咏于日常诗意

本文最后更新于:1 个月前

每次归程,都是为了更好出发;每次停歇,都是为了积攒力量

期盼

2024 年 12 月 21 日

冬至日。

忙碌的工作日已经,迎来工作生活后第一个周末的清晨。

对这份工作也不会抱有多大期待,只希望在最忙碌的时间里也不要忘记好好犒劳自己,在最难过的日子里也要能咬咬牙坚持下来。

昨天是这周的最后一个工作日,原本计划昨天上午完成这个栏目的前言,但奈何早上也没能抽出时间静下心来写点东西。

早上还看见高中那会儿的班级新建了一个班群,说是后续会有新的通知请及时关注,又是收集已毕业学子的那套把戏,不过没多少人填。

周四晚上情不自禁就把自己的荣誉和能力也统统写上去,还怂恿好兄弟也把自己精彩的校园经历整了上去。

这么看起来还算不错。

前天晚上刚吃完饭就跟爸妈视频聊了一会儿,昨天下班回家路上接到大哥的视频电话,取完快递回家后又跟正在备考期末的好兄弟语音两小时。

我的家人,永远是我最坚强的后盾,一想到即使远在他乡也有远方的人时刻挂念着自己,我便无所畏惧。

一介平凡人,尽力了便好。

下午太阳出来以后出门走走,冬至日里也要让自己吃上热乎的饺子。

努力工作,快乐生活。

泰豪软件

苇湖梁

2024 年 12 月 17 日

昨天下午一直在看核心代码,也没看出个所以然来,没有申请下账号密码不能登录 fts 代码仓,只好直接解压开心给的代码压缩包。

iois-whl-svr - Repos (tellhowsoft.com)

今早用薛松的账号登录 fts 代码仓,尝试拉取代码。

一直报错:

组长,石彤彤,王开心,康春辉帮忙看问题所在,搞了一个多小时。

image-20241217122313978

康春辉帮忙给发了这么个配置文件,放在本地公钥所在目录下,就能正常使用 SSH 地址的方式拉取代码到本地。

image-20241217113108507

配置文件内容:

1
2
3
4
Host dev.tellhowsoft.com
HostName dev.tellhowsoft.com
HostKeyAlgorithms +ssh-rsa
PubkeyAcceptedKeyTypes +ssh-rsa

拉取完成,新增分支,IDEA切换分支,就可以专心搞开发了。

image-20241217113333965

扒一扒Nacos、OpenFeign、Ribbon、loadbalancer组件协调工作的原理大家好,我是三友~~ 前几 - 掘金 (juejin.cn)

接口文档

2024 年 12 月 19 日

所有模块接口的文档都拿到了,网页均保存至本地集锦。

image-20241219110033332

image-20241219175257598

Springboot 系列(十六)你真的了解 Swagger 文档吗?-腾讯云开发者社区-腾讯云 (tencent.com.cn)

2024 年 12 月 24 日

解决springboot接入springfox-swagger2遇到的一些问题_java_脚本之家 (jb51.net)

一篇文章带你搞定 SpringBoot 整合 Swagger2_Java 开发学习最全合集-CSDN专栏

springboot整合swagger2_spring boot swagger2-CSDN博客.

SpringBoot集成Swagger 2 - luorx - 博客园 (cnblogs.com)

1
2
3
4
5
6
7
8
9
10
11
<!--swagger相关-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${swagger-version}</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>${swagger-version}</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.withClassAnnotation(RestController.class))
.paths(PathSelectors.any())
.build();
}

private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("许可证管理接口文档")
.description("许可证管理接口文档")
.version("1.0")
.build();
}
}

image-20241224175735003

org.springframework.context.ApplicationContextException: Failed to start bean ‘documentationPluginsBootstrapper’; nested exception is java.lang.NullPointerException - 哩个啷个波 - 博客园 (cnblogs.com)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
org.springframework.context.ApplicationContextException: Failed to start bean 'documentationPluginsBootstrapper'; nested exception is java.lang.NullPointerException
at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:181) ~[spring-context-5.3.24.jar:5.3.24]
at org.springframework.context.support.DefaultLifecycleProcessor.access$200(DefaultLifecycleProcessor.java:54) ~[spring-context-5.3.24.jar:5.3.24]
at org.springframework.context.support.DefaultLifecycleProcessor$LifecycleGroup.start(DefaultLifecycleProcessor.java:356) ~[spring-context-5.3.24.jar:5.3.24]
at java.lang.Iterable.forEach(Iterable.java:75) ~[na:1.8.0_211]
at org.springframework.context.support.DefaultLifecycleProcessor.startBeans(DefaultLifecycleProcessor.java:155) ~[spring-context-5.3.24.jar:5.3.24]
at org.springframework.context.support.DefaultLifecycleProcessor.onRefresh(DefaultLifecycleProcessor.java:123) ~[spring-context-5.3.24.jar:5.3.24]
at org.springframework.context.support.AbstractApplicationContext.finishRefresh(AbstractApplicationContext.java:935) ~[spring-context-5.3.24.jar:5.3.24]
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:586) ~[spring-context-5.3.24.jar:5.3.24]
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:147) ~[spring-boot-2.7.7.jar:2.7.7]
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:731) [spring-boot-2.7.7.jar:2.7.7]
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:408) [spring-boot-2.7.7.jar:2.7.7]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:307) [spring-boot-2.7.7.jar:2.7.7]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1303) [spring-boot-2.7.7.jar:2.7.7]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1292) [spring-boot-2.7.7.jar:2.7.7]
at com.license.server.LicenseServerApplication.main(LicenseServerApplication.java:10) [classes/:na]
Caused by: java.lang.NullPointerException: null
at springfox.documentation.spi.service.contexts.Orderings$8.compare(Orderings.java:112) ~[springfox-spi-2.9.2.jar:null]
at springfox.documentation.spi.service.contexts.Orderings$8.compare(Orderings.java:109) ~[springfox-spi-2.9.2.jar:null]
at com.google.common.collect.ComparatorOrdering.compare(ComparatorOrdering.java:37) ~[guava-20.0.jar:na]
at java.util.TimSort.countRunAndMakeAscending(TimSort.java:355) ~[na:1.8.0_211]
at java.util.TimSort.sort(TimSort.java:220) ~[na:1.8.0_211]
at java.util.Arrays.sort(Arrays.java:1438) ~[na:1.8.0_211]
at com.google.common.collect.Ordering.sortedCopy(Ordering.java:855) ~[guava-20.0.jar:na]
at springfox.documentation.spring.web.plugins.WebMvcRequestHandlerProvider.requestHandlers(WebMvcRequestHandlerProvider.java:57) ~[springfox-spring-web-2.9.2.jar:null]
at springfox.documentation.spring.web.plugins.DocumentationPluginsBootstrapper$2.apply(DocumentationPluginsBootstrapper.java:138) ~[springfox-spring-web-2.9.2.jar:null]
at springfox.documentation.spring.web.plugins.DocumentationPluginsBootstrapper$2.apply(DocumentationPluginsBootstrapper.java:135) ~[springfox-spring-web-2.9.2.jar:null]
at com.google.common.collect.Iterators$7.transform(Iterators.java:750) ~[guava-20.0.jar:na]
at com.google.common.collect.TransformedIterator.next(TransformedIterator.java:47) ~[guava-20.0.jar:na]
at com.google.common.collect.TransformedIterator.next(TransformedIterator.java:47) ~[guava-20.0.jar:na]
at com.google.common.collect.MultitransformedIterator.hasNext(MultitransformedIterator.java:52) ~[guava-20.0.jar:na]
at com.google.common.collect.MultitransformedIterator.hasNext(MultitransformedIterator.java:50) ~[guava-20.0.jar:na]
at com.google.common.collect.ImmutableList.copyOf(ImmutableList.java:249) ~[guava-20.0.jar:na]
at com.google.common.collect.ImmutableList.copyOf(ImmutableList.java:209) ~[guava-20.0.jar:na]
at com.google.common.collect.FluentIterable.toList(FluentIterable.java:614) ~[guava-20.0.jar:na]
at springfox.documentation.spring.web.plugins.DocumentationPluginsBootstrapper.defaultContextBuilder(DocumentationPluginsBootstrapper.java:111) ~[springfox-spring-web-2.9.2.jar:null]
at springfox.documentation.spring.web.plugins.DocumentationPluginsBootstrapper.buildContext(DocumentationPluginsBootstrapper.java:96) ~[springfox-spring-web-2.9.2.jar:null]
at springfox.documentation.spring.web.plugins.DocumentationPluginsBootstrapper.start(DocumentationPluginsBootstrapper.java:167) ~[springfox-spring-web-2.9.2.jar:null]
at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:178) ~[spring-context-5.3.24.jar:5.3.24]
... 14 common frames omitted

org.springframework.context.ApplicationContextException: Failed to start bean ‘documentationPluginsB-CSDN博客

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.withClassAnnotation(RestController.class))
.paths(PathSelectors.any())
.build();
}

private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("许可证管理接口文档")
.description("许可证管理接口文档")
.version("1.0")
.build();
}
}

2024 年 12 月 25 日

怪了,昨晚能成功启动项目了,但接口文档怎么还是访问不了呢,其他接口访问正常,看来还是整合 Swagger 失败了。

image-20241225090517410

一篇文章带你搞定 SpringBoot 整合 Swagger2_Java 开发学习最全合集-CSDN专栏

SpringBoot集成Swagger 2 - luorx - 博客园 (cnblogs.com)

解决springboot接入springfox-swagger2遇到的一些问题_java_脚本之家 (jb51.net)

image-20241225093647424

就这个文档对症下药了,果然是 Spring Boot 较高版本(2.6以上)整合 Swagger2 就会出现报错,三种解决方案:

1、ShortVideoSwagger2的配置类需要集成WebMvcConfigurationSupport

2、ShortVideoSwagger2的配置类增加@EnableWebMvc注解

3、springboot的配置增加增加一下配置

1
spring.mvc.pathmatch.matching-strategy=ANT_PATH_MATCHER

通过试验得知,这三种接口都可以解决,但是前两种是有副总用的

@EnableWebMvc建议慎用,最后在非springboot项目中使用

前两种解决方案会破坏springboot对springwebmvc的自动装配,导致自定义的一些convertor或者ObjectMapper失效。

目前我的项目中是自定义的ObjectMapper失效。

所以最后使用第三种方式,后期的springboot版本的matching-strategy默认的改为了PATH_PATTERN_PARSER,把它改为ANT_PATH_MATCHER就可以了。

这下子项目确实能够正常启动了,并且 Swagger 文档还特么生效了,完美解决问题,总结下 SpringBoot 集成 Swagger2 的步骤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<properties>
<swagger-version>2.9.2</swagger-version>
<swagger-ui.version>1.9.6</swagger-ui.version>
</properties>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${swagger-version}</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>${swagger-version}</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Configuration
@EnableSwagger2
@EnableSwaggerBootstrapUI
public class SwaggerConfig {
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.withClassAnnotation(RestController.class))
.paths(PathSelectors.any())
.build();
}

private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("许可证管理接口文档")
.description("许可证管理接口文档")
.version("1.0")
.build();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@RestController
@Api(value = "License 授权校验", tags = "License 授权校验")
@RequestMapping("/license")
public class LicenseVerifyController {
@Resource
private LicenseVerifyService licenseVerifyService;

/**
* License 证书上传
*
* @return LicenseContent
*/
@ApiOperation(value = "License 证书上传", notes = "License 证书上传")
@PostMapping("/upload")
public CommonResult<Boolean> upload(MultipartFile file) {

........................................
}

......................................
}

如果出现报错就按照上面提到的方案,配置文件里增加配置即可一步到位解决,现在接口文档总算可正常查看了。

1
spring.mvc.pathmatch.matching-strategy=ANT_PATH_MATCHER

image-20241225094907426

2024 年 12 月 30 日

Swagger访问路径添加前缀_swagger prefix-CSDN博客

过滤器(filter)和拦截器(Interceptor)的区别以及使用场景_过滤器和拦截器的区别和使用场景-CSDN博客

2025 年 1 月 7 日

【Spring Boot】Swagger接口分组及细分排序问题详解-腾讯云开发者社区-腾讯云 (tencent.com)

Nacos

2024 年 12 月 17 日

再学一学 Nacos 作为注册中心和配置中心的使用经验吧。

Nacos作为配置中心-CSDN博客

image-20241217131133110

实战:Nacos配置中心的Pull原理,附源码在单体服务时代,关于配置信息,管理一套配置文件即可。 而拆分成微服务之后, - 掘金 (juejin.cn)

SpringCloud之Nacos2.X作为配置中心_nacos配置数据库-CSDN博客

1
2
3
4
配置中心的信息一般都是放在bootstrap.yml 中;
初始化的时候,Bootstrap Context负责从外部源加载配置属性并解析配置;
Bootstrap属性有高优先级,默认情况下,它们不会被本地配置覆盖;
然后再读取application.yml中的配置,进行配置合并,完成项目的启动。

(九)漫谈分布式之微服务组件篇:探索分布式环境下各核心组件的必要性!记得几年前团队在招聘时,要求候选人有分布式/微服务项 - 掘金 (juejin.cn)

一如既往的高质量佳作。

特么一百一十五张表。。

image-20241217145043979

两点多看到三点半多,这代码看多了头昏脑胀的,耍会儿摸鱼网站。

软件License授权原理软件License授权原理 你知道License是如何防止别人破解的吗?本文将介绍Licens - 掘金 (juejin.cn)

Spring Boot实现License生成和校验1.License应用场景 在我们向客户销售商业软件的时候,常常需要对 - 掘金 (juejin.cn)

springboot增加license授权认证环境 MacOS 10.14.6 JDK1.8 源码链接: https:/ - 掘金 (juejin.cn)

2024 年 12 月 19 日

Cause: com.zaxxer.hikari.pool.HikariPool$PoolInitializationException: Failed to initialize pool-CSDN博客

解决启动微服务时报 WNAVAILABE: Unable to resolve host XXX_unavailable: unable to resolve host host-nacos-ser-CSDN博客

image-20241219092119380

大部分原因是本地的host文件中没有配置正确的主机名和对应的 IP 地址,要与远程服务器的一致才能够运行成功

一般都是在C:\Windows\System32\drivers\etc地址下的host文件中添加或修改自己项目对应的主机名和IP地址 例如:xxx.xxx.xxx.xxx:主机名

image-20241219092802777

1
2
3
# This line is auto added by aTrustAgent, do not modify, or aTrustAgent may unable to work
127.0.0.1 localhost.sangfor.com.cn
192.168.118.118 nacos.tellhow.com

那远程数据库连接失败的问题也就能解决了,同理。

image-20241219093107518

1
2
3
4
# This line is auto added by aTrustAgent, do not modify, or aTrustAgent may unable to work
127.0.0.1 localhost.sangfor.com.cn
192.168.118.118 nacos.tellhow.com
192.168.118.118 mysql.tellhow.com

就这么折腾一番,项目果然能在本地跑起来了,测试日志也顺利打印。

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>

测试下 Nacos 提供的示例代码,打印配置:

image-20241219094455403

这个测试暂时还跑不起来,先尝试本地启动所有模块吧。

1
2
3
4
5
6
# This line is auto added by aTrustAgent, do not modify, or aTrustAgent may unable to work
127.0.0.1 localhost.sangfor.com.cn
192.168.118.118 nacos.tellhow.com
192.168.118.118 mysql.tellhow.com
192.168.118.118 rabbitmq.tellhow.com
192.168.118.118 redis.tellhow.com

IDEA控制台不同类型日志显示不同颜色_idea设置控制台输出日志颜色-CSDN博客

扒一扒Nacos、OpenFeign、Ribbon、loadbalancer组件协调工作的原理大家好,我是三友~~ 前几 - 掘金 (juejin.cn)

License

基础学习

2024 年 12 月 18 日

昨天申请的内部账号使用权限已经按开通了,看见流程已走完,但账号密码没有。

image-20241218084641439

我真的了解泛型吗?前言 曾经年少轻狂的我,以为自己已经轻松拿捏Java泛型,直到后来学习Java 8新特性时,被泛型轻松 - 掘金 (juejin.cn)

昨晚睡觉前还花了二十来分钟时间,研究早上组长给我交代的问题:

springboot增加license授权认证环境 MacOS 10.14.6 JDK1.8 源码链接: https:/ - 掘金 (juejin.cn)

软件License授权原理软件License授权原理 你知道License是如何防止别人破解的吗?本文将介绍Licens - 掘金 (juejin.cn)

Springboot-软件授权License在我们做系统级框架的时候,我们要一定程度上考虑系统的使用版权,不能随便一个人 - 掘金 (juejin.cn)

Spring Boot项目中使用 TrueLicense 生成和验证License(服务器许可)License,即版权许 - 掘金 (juejin.cn)

生成公钥私钥

  • 使用JDK自带的keytool工具生成签名

    1
    keytool -genkeypair -keysize 1024 -validity 3650 -alias "zuiyuPrivateKey" -keypass "zuiyu_private_password_1234" -keystore "zuiyuPrivateKeys.keystore" -storepass "zuiyu_public_password_1234" -dname "CN=zuiyu,OU=zuiyu,O=zuiyu,L=BJ,ST=BJ,C=CN"
  • 导出签名文件 zuiyuCertfile.cer

    1
    keytool -exportcert -alias "zuiyuPrivateKey" -keystore "zuiyuPrivateKeys.keystore" -storepass "zuiyu_public_password_1234" -file "zuiyuCertfile.cer"
  • 导入签名文件

    1
    keytool -import -alias "zuiyuPublicCert" -file "zuiyuCertfile.cer" -keystore "zuiyuPublicCerts.keystore" -storepass "zuiyu_public_password_1234"
  • 帮助命令(根据需要食用)

    1
    2
    3
    4
    5
    text 代码解读复制代码# 删除
    keytool -delete -alias zuiyuPrivateKey -keystore "zuiyuPrivateKeys.keystore" -storepass "zuiyu_public_password_1234"

    # 查看
    keytool -list -v -keystore zuiyuPrivateKeys.keystore -storepass "zuiyu_public_password_1234"
  • 最后

    上述命令执行完成之后,会在当前路径下生成三个文件,分别是:zuiyuPrivateKeys.keystorezuiyuPublicCerts.keystorezuiyuCertfile.cer。其中文件zuiyuCertfile.cer不再需要可以删除,文件zuiyuPrivateKeys.keystore用于当前的license-server项目给客户生成license文件,而文件zuiyuPublicCerts.keystore则随应用代码部署到客户服务器,用户解密license文件并校验其许可信息。

生成 license 文件:

image-20241218090940296

新建工程,License,新增三个模块:license-common,license-server,license-client。

打包 license-common 模块,JDK 版本不兼容,Maven 版本过低(低于 3.6.3)

has been compiled by a more recent version of the Java Runtime (class file version 55.0)解决方法-CSDN博客

IDEA 设置里更改项目使用的 jdk 版本同本机环境变量里配置的 jdk 版本一致,我都更改版本为 jdk17,没有问题。

Maven 官方网站 - Download Apache Maven – Maven

Maven 版本过低的话,上 Maven 官网,直接下载这个就行:

image-20241218102703932

license-common 模块打包成功:

image-20241218102557020

原来这个@Value注解搞错了,导入的是 lombok 的 @Value 注解,怪不得读取不到配置文件:

1
2
3
4
5
spring.application.name=license-server
# license 默认生成地址
license.licensePath=/Users/cxt/Downloads/license/license.lic
# 授权签名文件相对项目的相对路径
license.privateKeysStorePath=/springboot-license/license-server/license/zuiyuPrivateKeys.keystore

image-20241218110152396

在您的代码中,出现“找不到符号”错误的原因是因为您混淆了Spring的@Value注解和Lombok的@Value注解。

  1. Spring的@Value注解:用于将配置文件(如application.propertiesapplication.yml)中的值注入到Spring管理的bean的字段中。这是您想要使用的注解,以便从配置文件中读取licensePathprivateKeysStorePath的值。
  2. Lombok的@Value注解:这是Lombok库提供的一个注解,用于自动为类的字段生成getter方法(以及可能的setter和equals/hashCode/toString方法),但它并不用于注入配置值。

从您的代码和描述来看,您想要使用Spring的@Value注解来注入配置值,但是可能由于以下原因之一导致编译错误:

  • 导入错误:您可能不小心导入了Lombok的@Value注解而不是Spring的@Value注解。确保您的导入语句是:

    1
    import org.springframework.beans.factory.annotation.Value;

特么起了项目也请求不到接口,我得再调试一番。

image-20241218113041983

body 请求体中带个 {} 就行了。空参。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 初始化证书生成参数
*
* @return de.schlichtherle.license.LicenseParam
*/
private LicenseParam initLicenseParam() {
Preferences preferences = Preferences.userNodeForPackage(LicenseCreator.class);

//设置对证书内容加密的秘钥
CipherParam cipherParam = new DefaultCipherParam(param.getStorePass());

KeyStoreParam privateStoreParam = new CustomKeyStoreParam(LicenseCreator.class
, param.getPrivateKeysStorePath()
, param.getPrivateAlias()
, param.getStorePass()
, param.getKeyPass());

LicenseParam licenseParam = new DefaultLicenseParam(param.getSubject()
, preferences
, privateStoreParam
, cipherParam);

return licenseParam;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"subject": "zuiyu_demo",
"privateAlias": "zuiyuPrivateKey",
"keyPass": "zuiyu_private_password_1234",
"storePass": "zuiyu_public_password_1234",
"licensePath": "/Users/cxt/Downloads/license/license7.lic",
"privateKeysStorePath": "/Users/cxt/Downloads/license/privateKeys.keystore",
"issuedTime": "2018-07-10 00:00:01",
"expiryTime": "2022-12-31 23:59:59",
"consumerType": "User",
"consumerAmount": 1,
"description": "这是证书描述信息",
"licenseCheckModel": {
"checkIp":false,
"ipAddress": [""],
"checkMac":true,
"macAddress": ["aa-bb-cc-11"],
"checkCpu":false,
"cpuSerial": "",
"checkMainBoard":false,
"mainBoardSerial": ""
}
}

image-20241218140433269

证书生成成功。

1
2
3
4
5
spring.application.name=license-server
# license ??????
license.licensePath=C:/Users/Lenovo/Downloads/license/license.lic
# ???????????????
license.privateKeysStorePath=/license-server/license/zuiyuPrivateKeys.keystore

image-20241218150133157

1
只有拥有私钥的系统(即license的生成者)才能生成有效的license文件,而任何拥有公钥证书的系统(即license的验证者)都可以验证license文件的真实性和完整性。

项目完善

2024 年 12 月 19 日

把 Demo 环境改换成 JDK8,看看还能不能跑起来。可以了。

javalicense授权工作流程 javalicense验证_mob64ca1403c772的技术博客_51CTO博客

这样的报错,就是两个 Controller 层所设置 @RequestMapping 映射路径相同导致的:

1
2
3
4
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'requestMappingHandlerMapping' defined in class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]: Ambiguous mapping. Cannot map 'licenseCreatorController' method 
com.memory.licenseserver.controller.LicenseCreatorController#generateLicense2(HttpServletRequest, LicenseCreatorParam)
to { [/license/generateLicense], produces [application/json;charset=UTF-8]}: There is already 'licenseCreatorController' bean method
com.memory.licenseserver.controller.LicenseCreatorController#generateLicense(HttpServletRequest) mapped.

image-20241219141707246

初始化证书生成参数,日期字符串转换日期:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 初始化证书生成参数
*
* @param param
*/
public void setParam(LicenseCreatorParam param) {
param.setSubject(this.subject);
param.setPrivateAlias(this.privateAlias);
param.setKeyPass(this.keyPass);
param.setStorePass(this.storePass);
param.setLicensePath(this.licensePath);
param.setPrivateKeysStorePath(System.getProperty("user.dir") + this.privateKeysStorePath);
param.setIssuedTime(new Date());

try {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
param.setExpiryTime(sdf.parse(this.expiryTime));
} catch (ParseException e) {
throw new RuntimeException(e);
}
param.setConsumerType(this.consumerType);
param.setConsumerAmount(this.consumerAmount);
param.setDescription(this.description);
param.setLicenseCheckModel(new LicenseCheckModel());
}

这日期,老是提示转换格式错误,把 Date 改成 String 就行了,读取配置后转换格式错误的,还以为上面初始化那会儿出错的。

1
2
3
4
5
6
7
8
9
10
11
12
license:
subject: zuiyu_demo
private-alias: zuiyuPrivateKey
key-pass: zuiyu_private_password_1234
store-pass: zuiyu_public_password_1234
issued-time: 2022-12-31 23:59:59
expiry-time: 2024-12-31 23:59:59
consumer-type: User
consumer-amount: 1
description: 这是证书描述信息
license-path: C:/Users/Lenovo/Downloads/license/license.lic
private-keys-store-path: /license-server/license/zuiyuPrivateKeys.keystore

image-20241219144157997

今下午,就这两个文档,帮了大忙:

javalicense授权工作流程 javalicense验证_mob64ca1403c772的技术博客_51CTO博客

Spring Boot应用启动时自动执行代码的五种方式_springboot启动后执行一段代码-CSDN博客

自启动:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@SpringBootApplication
public class LicenseServerApplication implements ApplicationRunner {

public static void main(String[] args) {
SpringApplication.run(LicenseServerApplication.class, args);
}

@Override
public void run(ApplicationArguments args) throws Exception {
// 在应用启动后执行的代码
System.out.println("do something 55555555");
System.out.println("ApplicationRunner启动");
System.out.println("===============");
}
}

这问题不会又绕回来了吧,得把 Controller 层的代码迁移到 LicenseCreator 中,还得保证正确读取到配置文件中的配置。

妈的,当然读取不到了,我竟然想用其他模块的类读取自己模块下的配置文件,,直接迁移 LicenseCreator:

image-20241219150007389

编译模块出现这样的问题,是 JDK 版本和 Maven 版本不兼容导致的,JDK 降成 8,看来 Maven需要再用 3.6.1 了

image-20241219151009375

license-common 模块,打包配置,不需要多加别的什么配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>8</source>
<target>8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>

Intellij IDEA install/package编译项目代码时,无效的标记: –release 的解决办法-CSDN博客

是 Spring Boot 版本不兼容 JDK 8 了。。。

使用@Value注解无法成功获取配置文件内容,常见原因_value注解读取不到配置-CSDN博客

不能降级 SpringBoot啊,只好是再改回来 JDK8。

maven出现:Failed to execute goal on project …: Could not resolve dependencies for project …-CSDN博客

1
2
3
4
5
6
7
8
9
Properties prop = new Properties();
Reader reader = null;
try {
InputStream in = new FileInputStream("License/license-server/src/main/resources/application.properties");
reader = new InputStreamReader(in, "UTF-8");
prop.load(reader);
} catch (IOException e) {
throw new RuntimeException(e);
}

加载properties资源配置文件_properties加载配置文件-CSDN博客

我滴妈,就这么一个简单的读取配置文件,搞了一下午吗,我真得再巩固下 Spring Boot 基础知识了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* 初始化证书生成参数
*
* @param param
*/
public void setParam(LicenseCreatorParam param) {
Properties prop = new Properties();
try {
prop.load(LicenseCreator.class.getResourceAsStream("/application.properties"));
} catch (IOException e) {
throw new RuntimeException(e);
}
param.setSubject(prop.getProperty("license.subject"));
param.setPrivateAlias(prop.getProperty("license.private-alias"));
param.setKeyPass(prop.getProperty("license.key-pass"));
param.setStorePass(prop.getProperty("license.store-pass"));
param.setLicensePath(prop.getProperty("license.license-path"));
param.setPrivateKeysStorePath(System.getProperty("user.dir") + prop.getProperty("license.private-keys-store-path"));
param.setIssuedTime(new Date());

try {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
param.setExpiryTime(sdf.parse(prop.getProperty("license.expiry-time")));
} catch (ParseException e) {
throw new RuntimeException(e);
}
param.setConsumerType(prop.getProperty("license.consumer-type"));
param.setConsumerAmount(Integer.parseInt(prop.getProperty("license.consumer-amount")));
param.setDescription(prop.getProperty("license.description"));
param.setLicenseCheckModel(new LicenseCheckModel());
}
1
2
3
4
5
6
7
8
9
10
11
12
# license
license.subject=zuiyu_demo
license.private-alias=zuiyuPrivateKey
license.key-pass=zuiyu_private_password_1234
license.store-pass=zuiyu_public_password_1234
license.issued-time=2022-12-31 23:59:59
license.expiry-time=2024-12-31 23:59:59
license.consumer-type=User
license.consumer-amount=1
license.description=这是证书描述信息
license.license-path=C:/Users/Lenovo/Downloads/license/license.lic
license.private-keys-store-path=/license-server/license/zuiyuPrivateKeys.keystore

先提交推送这部分代码吧,这一天也算没白干。

image-20241219170418414

捋一捋这个项目的生效思路:

其实一下午更多的时间里都在搞环境不兼容问题,JDK,Maven 甚至 Spring Boot 版本都在一直切换,因为早些时候想着要同现有项目环境兼容。

现在看来,把 License 这个模块单独抽象成一个 SDK,将来在现有项目模块中一键调用即可。

使用 JDK 自带的keytool工具生成签名文件。

项目启动后,读取本机配置文件信息,包括证书主体,密钥密码,生效期限,本机 MAC 地址,CPU 序列号等等,在指定目录下生成 License 证书。

客户端安装证书,并定期定时验证证书,证书验证通过则功能正常,直到超出生效期限后可以重新安装,不通过否则告警,功能关闭。

考虑到将来项目代码是部署在 Docker 环境中,那么证书的生成,安装和验证会不会有所不同,还有待验证。

现在需要在本机上跑通整个项目流程。

启动 license-client 失败了竟然。

1
java.lang.NullPointerException: Cannot invoke "java.lang.Boolean.booleanValue()" because the return value of "com.memory.common.bean.LicenseCheckModel.getCheckIp()" is null

image-20241219173027705

那就支持获取本机硬件信息:

1
2
3
4
// 扩展校验服务器硬件信息
String osName = System.getProperty("os.name");
LicenseCheckModel serverInfos = this.getServerInfos(osName);
param.setLicenseCheckModel(serverInfos);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 扩展校验服务器硬件信息
*
* @param osName
* @return
*/
private LicenseCheckModel getServerInfos(String osName) {
// 操作系统类型
if (StringUtils.isBlank(osName)) {
}
osName = osName.toLowerCase();
AbstractServerInfos abstractServerInfos = null;

// 根据不同操作系统类型选择不同的数据获取方法
if (osName.startsWith("windows")) {
abstractServerInfos = new WindowsServerInfos();
} else if (osName.startsWith("linux")) {
abstractServerInfos = new LinuxServerInfos();
} else {// 其他服务器类型
abstractServerInfos = new LinuxServerInfos();
}

return abstractServerInfos.getServerInfos();
}

报错原因被我找着了,看来是因为这四个字段没有默认值,之前调用接口的时候都有传参数的,这里就默认需要检查 License 证书里包含的本机硬件信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 可被允许的IP地址
*/
private Boolean checkIp = true;
private List<String> ipAddress;

/**
* 可被允许的 MAC 地址
*/
private Boolean checkMac = true;
private List<String> macAddress;

/**
* 可被允许的 CPU 序列号
*/
private Boolean checkCpu = true;
private String cpuSerial;

/**
* 可被允许的主板序列号
*/
private Boolean checkMainBoard = true;
private String mainBoardSerial;

image-20241219174435046

服务端优化

2024 年 12 月 20 日

今天早上继续完善这个功能,阳哥找我聊了下开发进度,我基本捋清楚了这个项目的业务流程,果然着眼于用户需求从客户的角度出发,才能解决问题。

用技术实现功能,解决实际问题。

Memory/license-server (gitee.com)

1
2
3
4
5
6
<!-- License -->
<dependency>
<groupId>de.schlichtherle.truelicense</groupId>
<artifactId>truelicense-core</artifactId>
<version>1.33</version>
</dependency>

image-20241220111100058

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.time.LocalDateTime;
import java.time.Duration;
import java.time.format.DateTimeFormatter;

public class CertificateExpirationCalculator {
public static void main(String[] args) {
// 假设 LicenseCreatorParamDTO 提供了这个参数
int validityDays = 365; // 证书的生效时长,例如一年

// 获取当前时间
LocalDateTime effectiveTime = LocalDateTime.now();

// 计算失效时间
LocalDateTime expirationTime = effectiveTime.plus(Duration.ofDays(validityDays));

// 格式化输出时间(可选)
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String effectiveTimeStr = effectiveTime.format(formatter);
String expirationTimeStr = expirationTime.format(formatter);

// 打印结果
System.out.println("生效时间: " + effectiveTimeStr);
System.out.println("失效时间: " + expirationTimeStr);
}
}

LicenseContent 类里面的日期类型都是 Date,看来需要封装一个方法转换 LocalDateTime 为 Date 了:

1
2
3
licenseContent.setIssued(param.getIssuedTime());
licenseContent.setNotBefore(param.getIssuedTime());
licenseContent.setNotAfter(param.getExpiryTime());

下午两点多,核心功能总算开发完成,粘贴下核心代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 生成 License 证书
*
* @param param 生成证书所需参数
* @return 生成结果
*/
@PostMapping(value = "/generateLicense")
public BaseResponse<Boolean> generateLicense(@RequestBody LicenseCreatorParamDTO param) {
// 1.校验 Controller 层参数
ThrowUtils.throwIf(ObjectUtils.isEmpty(param), ErrorCode.PARAMS_ERROR, "生成证书所需参数不能为空");
// 2.生成证书
boolean result = licenseCreateService.generateLicense(param);
// 3.返回结果
return ResultUtils.success(result);
}

service 层:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 生成 License 证书
*
* @return boolean
*/
public boolean generateLicense(LicenseCreatorParamDTO param) {
// 1.校验参数
this.checkLicense(param);
// 2.设置证书生成参数
this.setParam(param);
// 3.生成证书
return this.generate();
}

找不着生成路径:

1
2
3
4
5
6
7
8
# license
license.private-alias=zuiyuPrivateKey
license.key-pass=zuiyu_private_password_1234
license.store-pass=zuiyu_public_password_1234
license.consumer-type=User
license.consumer-amount=1
license.license-path=resourses/license/license.lic
license.private-keys-store-path=/license-server/license/zuiyuPrivateKeys.keystore

image-20241220143348705

这又找不着生成目录了:

1
System.getProperty("user.dir")

image-20241220143817107

特么 resources 写成 resourses 了。。

1
license.license-path=/resources/license/license.lic

艹。

1
2
license.license-path=/src/main/resources/license/license.lic
license.private-keys-store-path=/license/zuiyuPrivateKeys.keystore

下午三点四十六分,基本完成这个模块的编写了,测试也圆满完成,稍作休息,开启下一步。

image-20241220172208680

2024 年 12 月 23 日

上周实现的服务端生成证书参数还有些问题。

2024 年 12 月 14 日

保存证书信息:

证书名,所属项目,IP,MAC,许可期限,申请人,证书状态。

image-20241224112248215

证书生成,每个证书生成成功后都支持下载,证书的命名需要同绑定的项目模块相映射,服务端可保存多个有效证书。

客户端需更新下上传证书校验逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 生成 License 证书
*
* @return boolean
*/
public LicenseContent generateLicense(LicenseCreatorParamDTO param) {
// 1.校验参数
Boolean checkResult = this.checkLicense(param);
ThrowUtils.throwIf(!checkResult, ErrorCode.OPERATION_ERROR, "证书生成参数校验失败");
// 2.设置证书生成参数
Boolean setResult = this.setParam(param);
ThrowUtils.throwIf(!setResult, ErrorCode.OPERATION_ERROR, "证书生成参数设置失败");
// 3.生成并保存证书
LicenseContent licenseContent = this.generate();
ThrowUtils.throwIf(ObjectUtils.isEmpty(licenseContent), ErrorCode.OPERATION_ERROR, "证书生成失败");
return licenseContent;
}

生成证书,保存证书,需要建库建表,连接测试数据库。

证书下载,识别硬件信息,证书保存,接口文档,项目管理。

2024 年 12 月 25 日

证书保存。

一个小时过去了,证书保存功能趋于完善,首先需要优化证书生成过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 生成 License 证书
*
* @return boolean
*/
public LicenseContent generateLicense(LicenseCreatorParamDTO param) {
// 1.校验并设置证书生成参数
Boolean validated = this.validateAndSetParam(param);
ThrowUtils.throwIf(!validated, ErrorCode.PARAMS_ERROR, "证书生成参数校验失败");
// 2.生成并保存证书
LicenseContent licenseContent = this.generate();
ThrowUtils.throwIf(ObjectUtils.isEmpty(licenseContent), ErrorCode.OPERATION_ERROR, "证书生成失败");
return licenseContent;
}

这块业务有点特殊性,生成证书接口接收的传参需要用来生成证书(LicenseCreator),同样需要用来保存证书(LicenseInfo)。

然而这二者所需字段各不相同,所以提取要保存的证书信息(LicenseInfo)公共属性。

1
2
3
4
5
6
7
// 证书生成参数
@Resource
private LicenseCreatorParam param;
// 失效时间
int validityDays = 0;
// 证书信息
private final LicenseInfo licenseInfo = new LicenseInfo();
1
2
3
4
5
// 保存证书信息_1
licenseInfo.setLicenseName(param.getDescription());
licenseInfo.setProjectId(param.getProjectId());
licenseInfo.setApplicantName(param.getApplicantName());
licenseInfo.setValidityDays(this.validityDays);
1
2
3
4
5
6
7
8
9
10
11
12
13
// 4.保存证书信息_2
licenseInfo.setLicenseName(this.param.getDescription());
licenseInfo.setIpAddress(this.param.getLicenseCheckModel().getIpAddress());
licenseInfo.setMacAddress(this.param.getLicenseCheckModel().getMacAddress());
licenseInfo.setCpuSerial(this.param.getLicenseCheckModel().getCpuSerial());
licenseInfo.setMainBoardSerial(this.param.getLicenseCheckModel().getMainBoardSerial());
licenseInfo.setIssuedTime(this.convertTimeFormat(param.getIssuedTime()));
licenseInfo.setExpiryTime(this.convertTimeFormat(param.getExpiryTime()));
licenseInfo.setValidityDays(this.validityDays);
licenseInfo.setCreateTime(new Date());
licenseInfo.setUpdateTime(new Date());
boolean save = this.save(licenseInfo);
ThrowUtils.throwIf(!save, ErrorCode.OPERATION_ERROR, "证书信息保存失败");

今天早些时候的接口文档派上用场了,根据请求示例构造生成证书请求:

image-20241225154510734

image-20241225155732737

到目前为止其实能看到证书生成的全过程没有问题,本次新增了客户端硬件参数并优化了校验参数逻辑,这里问题出在持久化至数据库。

image-20241225155705205

很明显是由于 ipAddressmacAddress为 List 列表类型,保存至数据库需要序列化为 json,后续获取 License 证书信息需要反序列化。

引入 Gson GVA 坐标:

1
2
3
4
5
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
1
2
3
4
5
6
7
8
9
/**
* IP
*/
private String ipAddress;

/**
* MAC
*/
private String macAddress;
1
2
3
4
5
// 4.保存证书信息_2
Gson gson = new Gson();
licenseInfo.setLicenseName(this.param.getDescription());
licenseInfo.setIpAddress(gson.toJson(this.param.getLicenseCheckModel().getIpAddress()));
licenseInfo.setMacAddress(gson.toJson(this.param.getLicenseCheckModel().getMacAddress()));

上个厕所,喝口水,显然已经成功了,新的证书绑定了客户端主机硬件参数,同时成功保存证书至 MySQL 数据库中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"applicantName": "黄天柱",
"description": "新疆北疆三站证书",
"licenseCheckModel": {
"checkCpu": true,
"checkIp": true,
"checkMac": true,
"checkMainBoard": true,
"cpuSerial": "BFEBFBFF000806C1",
"ipAddress": [
"192.168.116.157",
"192.168.88.1"
],
"macAddress": [
"80-45-DD-E3-6E-DF",
"00-50-56-C0-00-08"
],
"mainBoardSerial": "YX02JN9N"
},
"projectId": "TH(G)0101012312002",
"subject": "zuiyu_demo",
"validityDays": 30
}

image-20241225162312403

image-20241225162151403

生成证书不需要传参主题 subject,本地配置写死即可:

1
2
# license
license.subject=zuiyu_demo

证书生成还应该优化下,签名文件和验签文件,应该如何生成,可以在证书生成过程中加载配置生成。

生成证书需根据专门设置证书名称存放,同名证书会被覆盖掉。

使用 UUID 作为证书名,从数据库中下载指定证书时应该传入证书序号以针对性下载,这都是很简单的业务逻辑。

获取证书列表初步完成,分页获取已生成的证书列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 获取 License 证书列表
*
* @param request 查询参数
*/
@ApiOperation(value = "获取 License 证书列表", notes = "获取 License 证书列表")
@PostMapping(value = "/getLicenseInfoList")
public BaseResponse<Page<LicenseInfo>> getLicenseInfoList(@RequestBody LicenseInfoQueryRequest request) {
// 1.校验 Controller 层参数
ThrowUtils.throwIf(ObjectUtils.isEmpty(request), ErrorCode.PARAMS_ERROR, "生成证书所需参数不能为空");
// 2.获取证书列表
Page<LicenseInfo> licenseInfoList = licenseInfoService.getLicenseInfoList(request);
// 3.返回结果
return ResultUtils.success(licenseInfoList);
}
1
2
3
4
{
"currrent":"1",
"size":"10"
}

image-20241225173154766

2024 年 12 月 26 日

文档,请求参数,响应参数

证书下载,证书名。

响应参数:

1
2
3
4
// 5.返回结果
LicenseContentVO licenseContentVO = new LicenseContentVO();
BeanUtils.copyProperties(licenseContent, licenseContentVO);
return licenseContentVO;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
@Data
@ApiModel(value = "证书内容")
public class LicenseContentVO {
/**
* 证书持有者
*/
@ApiModelProperty(value = "证书持有者", required = true)
private X500Principal holder;
/**
* 证书颁发者
*/
@ApiModelProperty(value = "证书颁发者", required = true)
private X500Principal issuer;
/**
* 证书主题
*/
@ApiModelProperty(value = "证书主题", required = true)
private String subject;
/**
* 颁发日期
*/
@ApiModelProperty(value = "颁发日期", required = true)
private Date issued;
/**
* 有效期开始
*/
@ApiModelProperty(value = "有效期开始", required = true)
private Date notBefore;
/**
* 有效期结束
*/
@ApiModelProperty(value = "有效期结束", required = true)
private Date notAfter;
/**
* 消费者类型
*/
@ApiModelProperty(value = "消费者类型", required = true)
private String consumerType;
/**
* 消费者数量
*/
@ApiModelProperty(value = "消费者数量", required = true)
private int consumerAmount;
/**
* 附加信息(证书描述)
*/
@ApiModelProperty(value = "附加信息(证书描述)", required = true)
private String info;
/**
* 额外数据(客户端硬件信息)
*/
@ApiModelProperty(value = "额外数据(客户端硬件信息)", required = true)
private Object extra;
}

证书生成时,使用 UUID 实现设置证书序号,并以此证书序号命名证书。

1
2
license.license-path=/src/main/resources/license/
license.private-keys-store-path=/license/zuiyuPrivateKeys.keystore
1
2
String LicenseID = UUID.randomUUID().toString();
this.param.setLicensePath(Paths.get(LicenseConstant.USER_DIR, prop.getProperty("license.license-path"), LicenseID + ".lic").toString());

image-20241226111129819

1
2
3
4
5
/**
* 证书序号
*/
@TableId(type = IdType.ASSIGN_UUID)
private String id;

分模块后有个问题,本地工作目录又多了一层,但将来部署的时候是单个项目部署,路径可能不一致,不过考虑到打包是统一打包的,应该不会有路径问题。

我试试。

没有问题了。

1
2
3
4
5
6
7
8
9
# license
license.subject=zuiyu_demo
license.private-alias=zuiyuPrivateKey
license.key-pass=zuiyu_private_password_1234
license.store-pass=zuiyu_public_password_1234
license.consumer-type=User
license.consumer-amount=1
license.license-path=/license-server/src/main/resources/license/
license.private-keys-store-path=/license-server/license/zuiyuPrivateKeys.keystore

生成证书还得校验身份,管理员,下午完成登录模块后再回头优化。

证书下载,指定证书序号,下载对应证书。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 证书下载请求参数
*/
@Data
@ApiModel(value = "证书下载请求参数")
public class LicenseDownloadQuery {

/**
* 证书序号
*/
@ApiModelProperty(value = "证书序号", required = true)
private Long licenseId;
}

证书下载需要传递参数了,指定证书序号,有时间我得把这块儿逻辑放在 service 层,拖久了就成古老屎山代码了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/**
* 下载证书
*
* @param licenseDownloadQuery 下载参数
*/
@ApiOperation(value = "License 证书下载", notes = "License 证书下载")
@PostMapping("/download")
public BaseResponse<HttpServletResponse> downloadLicense(@RequestBody
LicenseDownloadQuery licenseDownloadQuery, HttpServletResponse response) {
// 本地文件路径
Properties prop = new Properties();
try {
prop.load(LicenseCreator.class.getResourceAsStream("/application.properties"));
} catch (IOException e) {
throw new RuntimeException(e);
}
// 证书下载路径
Long licenseId = licenseDownloadQuery.getLicenseId();
String filePath = Paths.get(LicenseConstant.USER_DIR, prop.getProperty("license.license-path"), licenseId + ".lic").toString();
// String filePath = System.getProperty("user.dir") + "/src/main/resources/license/new.png";
// 下载时文件名(客户端保存时看到的文件名)
String downloadFileName = "license.lic";
// String downloadFileName = "new.png";
try {
// 使用缓冲输入流读取本地文件
BufferedInputStream inputStream = new BufferedInputStream(Files.newInputStream(Paths.get(filePath)));
// 使用HttpServletResponse的输出流将文件发送给客户端
BufferedOutputStream outputStream = new BufferedOutputStream(response.getOutputStream());
// 设置响应头
response.setContentType("application/octet-stream"); // 设置为通用二进制文件类型
// response.setContentType("image/png"); // 根据文件类型设置正确的MIME类型
response.setHeader("Content-Disposition", "attachment; filename=\"" + downloadFileName + "\"");
response.setContentLength((int) new File(filePath).length()); // 可选:设置内容长度,有助于浏览器正确处理下载
// 读取文件内容并写入响应输出流
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
// 关闭流
outputStream.flush();
outputStream.close();
} catch (IOException e) {
throw new RuntimeException("文件下载失败", e);
}
// 返回响应
return ResultUtils.success(response);
}
1
2
3
// 证书下载路径
Long licenseId = licenseDownloadQuery.getLicenseId();
String filePath = Paths.get(LicenseConstant.USER_DIR, prop.getProperty("license.license-path"), licenseId + ".lic").toString();

先生成四个证书,看起来很完善的,证书均生成成功,证书信息也成功保存至数据库中。

image-20241226120554322

接下来就是下载证书了。

image-20241226121136849

搞错了,应该是 String 类型的。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 证书下载请求参数
*/
@Data
@ApiModel(value = "证书下载请求参数")
public class LicenseDownloadQuery {

/**
* 证书序号
*/
@ApiModelProperty(value = "证书序号", required = true)
private String licenseId;
}
1
2
3
// 证书下载路径
String licenseId = licenseDownloadQuery.getLicenseId();
String filePath = Paths.get(LicenseConstant.USER_DIR, prop.getProperty("license.license-path"), licenseId + ".lic").toString();

image-20241226121333598

1
2
3
{
"licenseId":"2729dbda-513d-4f63-b975-2c4a01d49522"
}

这么看来,应该是成功了。

艹。

1
2
3
org.springframework.http.converter.HttpMessageNotWritableException: No converter for [class com.license.server.common.BaseResponse] with preset Content-Type 'application/octet-stream'
at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:312) ~[spring-webmvc-5.3.24.jar:5.3.24]
at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.handleReturnValue(RequestResponseBodyMethodProcessor.java:183) ~[spring-webmvc-5.3.24.jar:5.3.24]

不知道控制台发什么神经,反正证书是拿到了:

1
2
3
4
5
6
7
8
POST http://localhost:8081/license-server/license/download
Content-Type: application/json

{
"licenseId":"2729dbda-513d-4f63-b975-2c4a01d49522"
}

<> license.lic

image-20241226121844878

证书生成,校验用户状态;证书下载,校验用户状态。

1
2
3
4
5
// 1.登录用户状态
UserInfo loginUser = userInfoService.getLoginUser(request);
ThrowUtils.throwIf(ObjectUtils.isEmpty(loginUser), ErrorCode.NOT_LOGIN_ERROR, "用户未登录");
// 2.管理员权限
ThrowUtils.throwIf(!UserRoleEnum.ADMIN.getText().equals(loginUser.getUserRole()), ErrorCode.NO_AUTH_ERROR, "非管理员, 无权限生成证书");
1
2
3
4
5
6
7
8
9
10
ThrowUtils.throwIf(ObjectUtils.isEmpty(licenseDownloadQuery), ErrorCode.PARAMS_ERROR, "证书序号不能为空");
QueryWrapper<LicenseInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("id", licenseDownloadQuery.getLicenseId());
int count = licenseInfoService.count(queryWrapper);
ThrowUtils.throwIf(count == 0, ErrorCode.NOT_FOUND_ERROR, "指定证书不存在");
// 1.登录用户状态
UserInfo loginUser = userInfoService.getLoginUser(request);
ThrowUtils.throwIf(ObjectUtils.isEmpty(loginUser), ErrorCode.NOT_LOGIN_ERROR, "用户未登录, 无法下载证书");
// 2.管理员权限
ThrowUtils.throwIf(!UserRoleEnum.ADMIN.getText().equals(loginUser.getUserRole()), ErrorCode.NO_AUTH_ERROR, "非管理员, 无权限下载证书");

管理员创建用户逻辑,优化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* 创建用户
*
* @param userInfoAddRequest
* @param request
* @return
*/
@ApiOperation(value = "创建用户", notes = "创建用户")
@PostMapping("/add")
public BaseResponse<Long> addUser(@RequestBody UserInfoAddRequest userInfoAddRequest, HttpServletRequest request) {
if (userInfoAddRequest == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数为空");
}
String userAccount = userInfoAddRequest.getUserAccount();
String userPassword = userInfoAddRequest.getUserPassword();
if (StringUtils.isAnyBlank(userAccount, userPassword)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数为空");
}
// 查询用户是否存在
QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user_account", userAccount);
int count = userInfoService.count(queryWrapper);
if (count > 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "账号重复");
}
// 2. 加密
UserInfo userInfo = new UserInfo();
BeanUtils.copyProperties(userInfoAddRequest, userInfo);
String encryptPassword = DigestUtils.md5DigestAsHex((UserConstant.SALT + userPassword).getBytes());
userInfo.setUserPassword(encryptPassword);

boolean result = userInfoService.save(userInfo);
ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR, "创建用户失败");
return ResultUtils.success(userInfo.getId());
}

顺便完善下用户注销:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 用户注销
*
* @param request
* @return
*/
@PostMapping("/logout")
@ApiOperation(value = "用户注销", notes = "用户注销")
public BaseResponse<Boolean> userLogout(HttpServletRequest request) {
if (request == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
boolean result = userInfoService.userLogout(request);
return ResultUtils.success(result);
}

2024 年 12 月 30 日

1
2
3
// 2.管理员权限
String text = UserRoleEnum.ADMIN.getValue();
String role = loginUser.getUserRole();

取错值了,getText 是“管理员”。

image-20241230090530134

客户端优化

2024 年 12 月 20 日

把基本的代码导入到 th-iois-common 和 th-iois-inspection模块中,准备进行下一步测试。

2024 年 12 月 23 日

证书上传

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 证书上传
*
* @return LicenseContent
*/
@PostMapping("/upload")
public CommonResult<Boolean> upload(MultipartFile file) {
// 1.校验 Controller 层参数
// 2.上传证书
Boolean result = licenseVerifyService.upload(file);
// 3.返回结果
return CommonResult.success(result);
}
1
2
3
4
5
6
7
8
9
# license-client
spring.application.name=license-client
server.port=8082
# license
license.subject=zuiyu_demo
license.public-alias=zuiyuPublicCert
license.store-pass=zuiyu_public_password_1234
license.license-path=/th-iois-inspection/license/license.lic
license.public-keys-store-path=/th-iois-inspection/license/zuiyuPublicCerts.keystore

分别上传证书和公钥:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* 证书上传
*
* @param file 文件
* @return Boolean
*/
@Override
public Boolean upload(MultipartFile file) {
// 1.上传文件
String originalFilename = file.getOriginalFilename();
Properties prop = new Properties();
assert originalFilename != null;

try {
// 获取配置文件
prop.load(LicenseCreator.class.getResourceAsStream("/application.properties"));
// 判断文件类型
// 上传证书
if (originalFilename.equals(LicenseConstant.LICENSE_FILE_NAME)) {
file.transferTo(Paths.get(LicenseConstant.USER_DIR, prop.getProperty("license.license-path")));
return true;
}
// 上传公钥
if (originalFilename.equals(LicenseConstant.PUBLIC_KEY_FILE_NAME)) {
file.transferTo(Paths.get(LicenseConstant.USER_DIR, prop.getProperty("license.public-keys-store-path")));
return true;
}
// 其他文件
} catch (IOException e) {
throw new RuntimeException(e);
}

// 2.返回结果
return false;
}

好极了,上传文件到客户端目录下已经测试完成:

image-20241223102312324

证书安装:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 证书安装
*
* @return LicenseContent
*/
@PostMapping("/install")
public CommonResult<LicenseContent> install() {
// 1.校验 Controller 层参数
// 2.安装证书
LicenseContent result = licenseVerifyService.install();
// 3.返回结果
return CommonResult.success(result);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 证书安装
*
* @return LicenseContent
*/
@Override
public LicenseContent install() {
// 设置证书安装参数
LicenseVerifyParam param = new LicenseVerifyParam();
param.setSubject(subject);
param.setPublicAlias(publicAlias);
param.setStorePass(storePass);
param.setLicensePath(Paths.get(LicenseConstant.USER_DIR, licensePath).toString());
param.setPublicKeysStorePath(Paths.get(LicenseConstant.USER_DIR, publicKeysStorePath).toString());
// 证书安装
return licenseVerify.install(param);
}

image-20241223104626568

这就安装成功了,看来上周做的工作很到位,到这一步为止基本没有什么问题。

证书验证。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 证书校验定时任务
*/
@Component
public class LicenseVerifyJob {
@Resource
private LicenseVerify licenseVerify;

@Scheduled(cron = "*/2 * * * * *")
public void getArticleContent() {
System.out.println("定时任务执行" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
}
}

证书校验定时任务执行,出问题了:

image-20241223111236083

1
2
3
4
5
6
7
8
9
10
@Resource
private LicenseVerifyService licenseVerifyService;

/**
* 每隔2秒执行一次
*/
@Scheduled(cron = "*/2 * * * * *")
public void getArticleContent() {
licenseVerifyService.verify();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 校验 License 证书
*
* @return boolean
*/
public boolean verify() {
LicenseManager licenseManager = LicenseManagerHolder.getInstance(null);
DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

// 2. 校验证书
try {
LicenseContent licenseContent = licenseManager.verify();
log.info(MessageFormat.format("证书校验通过,证书有效期:{0} - {1}", format.format(licenseContent.getNotBefore()), format.format(licenseContent.getNotAfter())));
return true;
} catch (Exception e) {
log.error("证书校验失败!", e);
return false;
}
}

怪了。

image-20241223114011429

再跑一遍上周的 Demo 代码,校验证书的这段逻辑现在是没有问题的。

1
2
3
4
5
log.info("进入拦截器,验证证书可使用性");
LicenseVerify licenseVerify = new LicenseVerify();

// 校验证书是否有效
boolean verifyResult = licenseVerify.verify();
1
LicenseManager licenseManager = LicenseManagerHolder.getInstance(null);
1
2
3
4
5
6
7
8
9
10
11
public static LicenseManager getInstance(LicenseParam param) {
if (LICENSE_MANAGER == null) {
synchronized (LicenseManagerHolder.class) {
if (LICENSE_MANAGER == null) {
LICENSE_MANAGER = new CustomLicenseManager(param);
}
}
}

return LICENSE_MANAGER;
}
1
2
3
public CustomLicenseManager(LicenseParam param) {
super(param);
}

待会儿外卖到了吃完中午饭,下午睡醒了再看吧。

1
2
3
4
5
6
/**
* License 校验类
*/
@Component
@Slf4j
public class LicenseVerify {
1
2
@Resource
private LicenseVerify licenseVerify;
1
2
3
4
5
6
7
8
/**
* 证书校验
*/
@Override
public void verify() {
// 证书校验
licenseVerify.verify();
}

下午,找到了问题所在:

为什么必须先安装证书才能校验成功?

  • 许可证状态:在您的应用程序中,LicenseManager 似乎负责管理许可证的状态。如果没有安装许可证,LicenseManager 将没有许可证信息可供校验。
  • 安装与校验的依赖verify 方法依赖于 LicenseManager 中已经存在的许可证信息。如果先不执行 install 方法来安装许可证,那么 verify 方法将无法找到任何要校验的许可证,因此会失败。
  • 程序逻辑:从程序逻辑上讲,您通常需要先安装许可证,然后才能校验它。这是因为校验是验证已安装许可证的有效性的过程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@PostConstruct
public void doSomething(){
// 在应用启动后, 执行的代码
System.out.println("do something");
System.out.println("do something");
System.out.println("do something");
}

/**
* 每隔2秒执行一次
*/
@Scheduled(cron = "*/2 * * * * *")
public void getArticleContent() {
System.out.println("定时任务执行" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
// licenseVerifyService.verify();
}

image-20241223134942033

应该在项目启动后,首先安装临时证书,才能保证定时执行证书校验任务的正常运行,这样的业务流程也合情合理。

Spring Boot应用启动时自动执行代码的五种方式_springboot启动后执行一段代码-CSDN博客

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 应用启动后, 执行
*/
@PostConstruct
public void doLicenseInstall() {
// 应用启动后, 执行临时证书安装
licenseVerifyService.install();
}

/**
* 定时任务
*/
@Scheduled(cron = "*/5 * * * * *")
public void doLicenseVerifyJob() {
// 每5秒校验一次证书有效性
licenseVerifyService.verify();
}

很显然运行成功了:

image-20241223140140358

1
2
3
4
'iois-file-server' URL not provided. Will try picking an instance via load-balancing.
dahuatech-icc- 2024-12-23 13:59:01 [main] INFO com.th.cloud.iois.common.license.license.LicenseVerify:23 - ++++++++ 开始安装证书 ++++++++
dahuatech-icc- 2024-12-23 13:59:02 [main] INFO com.th.cloud.iois.common.license.license.LicenseVerify:32 - 证书安装成功,证书有效期:2024-12-20 17:19:59 - 2025-01-19 17:19:59
dahuatech-icc- 2024-12-23 13:59:02 [main] INFO com.th.cloud.iois.common.license.license.LicenseVerify:38 - ++++++++ 证书安装结束 ++++++++
1
2
3
4
5
6
Listening config: dataId=license-client-dev.yaml, group=DEFAULT_GROUP
dahuatech-icc- 2024-12-23 13:59:15 [scheduling-1] INFO com.th.cloud.iois.common.license.license.LicenseVerify:54 - 证书校验通过,证书有效期:2024-12-20 17:19:59 - 2025-01-19 17:19:59
dahuatech-icc- 2024-12-23 13:59:20 [scheduling-1] INFO com.th.cloud.iois.common.license.license.LicenseVerify:54 - 证书校验通过,证书有效期:2024-12-20 17:19:59 - 2025-01-19 17:19:59
dahuatech-icc- 2024-12-23 13:59:25 [scheduling-1] INFO com.th.cloud.iois.common.license.license.LicenseVerify:54 - 证书校验通过,证书有效期:2024-12-20 17:19:59 - 2025-01-19 17:19:59
dahuatech-icc- 2024-12-23 13:59:30 [scheduling-1] INFO com.th.cloud.iois.common.license.license.LicenseVerify:54 - 证书校验通过,证书有效期:2024-12-20 17:19:59 - 2025-01-19 17:19:59
dahuatech-icc- 2024-12-23 13:59:35 [scheduling-1] INFO com.th.cloud.iois.common.license.license.LicenseVerify:54 - 证书校验通过,证书有效期:2024-12-20 17:19:59 - 2025-01-19 17:19:59

呐,th-iois-file-server 模块同样搞定了,只需要复制整个 license 模块下的代码,再针对性修改各个模块下的配置文件即可:

1
2
3
4
5
6
7
# license
license:
subject: zuiyu_demo
public-alias: zuiyuPublicCert
store-pass: zuiyu_public_password_1234
license-path: /th-iois-file-server/license/license.lic
public-keys-store-path: /th-iois-file-server/license/zuiyuPublicCerts.keystore
1
2
3
4
5
6
7
# license
license:
subject: zuiyu_demo
public-alias: zuiyuPublicCert
store-pass: zuiyu_public_password_1234
license-path: /th-iois-inspection/license/license.lic
public-keys-store-path: /th-iois-inspection/license/zuiyuPublicCerts.keystore

达梦数据库

2024 年 12 月 19 日

数据库对比系列之二(MySQL和达梦) - 墨天轮 (modb.pro)

达梦数据库跟mysql的区别_mob64ca12dedda8的技术博客_51CTO博客

国产达梦数据库与MySQL的区别 - OSCHINA - 中文开源技术交流社区

这公司是不是十一月底给我发面试邀约的那家。。base 武汉,对这家公司不太感兴趣,但泰豪项目中用到了达梦数据库。

达梦与主流数据库对比感悟 | 达梦技术社区 (dameng.com)

自启动

2024 年 12 月 19 日

Spring Boot应用启动时自动执行代码的五种方式_springboot启动后执行一段代码-CSDN博客

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@SpringBootApplication
public class LicenseServerApplication implements ApplicationRunner {

public static void main(String[] args) {
SpringApplication.run(LicenseServerApplication.class, args);
}

@Override
public void run(ApplicationArguments args) throws Exception {
// 在应用启动后执行的代码
System.out.println("do something 55555555");
System.out.println("ApplicationRunner启动");
System.out.println("===============");
}
}

Demo 代码如下

2024 年 12 月 23 日

  1. @PostConstruct注解
    @PostConstruct注解可以标注在方法上,该方法将在类被初始化后调用。在Spring Boot应用中,你可以使用这个注解来执行一些初始化的逻辑。
1
2
3
4
5
@PostConstruct
public void doSomething(){
// 在应用启动后执行的代码
System.out.println("do something");
}
  1. ApplicationListener接口
    实现ApplicationListener接口并监听ApplicationStartedEvent事件,这样你的逻辑将在应用启动后被触发。
1
2
3
4
5
6
7
8
9
10
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.ApplicationListener;

public class MyApplicationListener implements ApplicationListener<ApplicationStartedEvent> {
@Override
public void onApplicationEvent(ApplicationStartedEvent event) {
// 在应用启动后执行的代码
System.out.println("ApplicationListener executed");
}
}
  1. @EventListener注解
    使用@EventListener注解,可以将方法标记为事件监听器,并在特定事件发生时执行。
1
2
3
4
5
6
7
8
9
10
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.event.EventListener;

public class MyEventListener {
@EventListener(ApplicationStartedEvent.class)
public void onApplicationEvent() {
// 在应用启动后执行的代码
System.out.println("@EventListener executed");
}
}
  1. ApplicationRunner接口
    实现ApplicationRunner接口,该接口的run方法会在Spring Boot应用启动后执行。
1
2
3
4
5
6
7
8
9
10
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;

public class MyApplicationRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
// 在应用启动后执行的代码
System.out.println("ApplicationRunner executed");
}
}
  1. CommandLineRunner接口
    ApplicationRunner类似,CommandLineRunner接口的run方法也在应用启动后执行。
1
2
3
4
5
6
7
public class MyCommandLineRunner implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
// 在应用启动后执行的代码
System.out.println("CommandLineRunner executed");
}
}

导师

2024 年 12 月 20 日

阳哥这番话,醍醐灌顶。

Harbor

2024 年 12 月 20 日

harbor: Harbor 是为企业用户设计的容器镜像仓库开源项目,包括了权限管理(RBAC)、LDAP、审计、安全漏洞扫描、镜像验真、管理界面、自我注册、HA 等企业必需的功能,同时针对中国用户的特点,设计镜像复制和中文支持等功能。 (gitee.com)

image-20241220121750325

Harbor

Docker-harbor私有仓库_harbor和docker的关系-CSDN博客

harbor与docker关系 harbor docker_colddawn的技术博客_51CTO博客

部署Docker私有镜像仓库Harbor_Mingo的技术博客_51CTO博客

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
在企业项目中,常用的镜像仓库主要包括以下几种:

一、公有镜像仓库
Docker Hub
简介:Docker Hub是一个提供共享应用程序和自动化工作流工具的公共存储库。
功能:它允许开发者创建、测试、存储和共享Docker容器映像。用户可以使用Docker Hub来发现、共享和运行容器化应用程序。它还支持自动化构建、存储和部署,以及团队协作等功能。
二、私有镜像仓库(企业级镜像仓库)
Harbor
简介:Harbor是VMware公司开源的一个企业级Docker Registry项目,项目地址为https://github.com/goharbor/harbor。
功能:Harbor提供了一个安全、可信赖的仓库来存储和管理Docker镜像。它实现了基于角色的访问控制机制,通过项目来对镜像进行组织和访问权限的控制。此外,Harbor还提供了图形化的管理界面,方便用户通过浏览器来浏览和检索当前Docker镜像仓库,管理项目和命名空间。Harbor支持镜像的多用户管理、权限控制、镜像复制、镜像扫描、自动同步等功能,非常适合用于企业内部的镜像管理。
搭建:Harbor的搭建过程包括下载离线安装包、修改配置文件、修改Docker守护进程配置文件、启动安装等步骤。安装完成后,用户可以通过IP和端口访问Harbor的Web页面,进行镜像的上传、下载和管理。
JFrog Artifactory
简介:JFrog是一家知名的DevOps工具提供商,其Artifactory产品可以用作镜像仓库管理系统。
功能:Artifactory支持多种软件包格式,包括Docker镜像。它提供了强大的安全性、可伸缩性和管理功能,可以帮助开发团队更好地管理镜像。
Nexus Repository Manager
简介:Nexus Repository Manager是Sonatype公司推出的一款开源的存储管理工具。
功能:它支持多种包管理格式,包括Docker。Nexus提供了丰富的API和插件,可以与各种自动化工具和持续集成系统集成,方便用户进行镜像的存储和管理。
三、混合镜像仓库
除了公有镜像仓库和私有镜像仓库外,还有一些镜像仓库管理系统支持混合模式,即同时支持公有和私有镜像的存储和管理。这种混合模式可以根据企业的实际需求进行灵活配置和使用。

综上所述,企业项目在选择镜像仓库时,可以根据项目的具体需求、安全性要求、团队协作需求以及成本预算等因素进行综合考虑。Docker Hub、Harbor、JFrog Artifactory和Nexus Repository Manager等都是不错的选择,它们各有特点和优势,能够满足不同企业的需求。

字符串

2024 年 12 月 20 日

优雅拼接字符串,有哪些方法呢?

直接拼接:

1
this.param.setLicensePath(System.getProperty("user.dir") + prop.getProperty("license.license-path"));

设置分隔符:

1
this.param.setLicensePath(String.join("", LicenseConstant.USER_DIR, prop.getProperty("license.license-path")));

然而,更常见的情况是,如果privateKeysStorePath是一个文件名,而你需要将其附加到userDir后面形成一个完整的文件路径,那么你可能需要使用文件分隔符(在Windows上是\,在Unix/Linux/Mac上是/)。但Java的File类提供了跨平台处理文件路径的方法,因此更推荐使用File.separatorPaths类来构建路径。

File:

1
this.param.setLicensePath(new File(LicenseConstant.USER_DIR, prop.getProperty("license.license-path")).getPath());

Paths:

1
this.param.setLicensePath(Paths.get(LicenseConstant.USER_DIR, prop.getProperty("license.license-path")).toString());

这几种全部都测试过了,都能够正常拼接字符串,找到文件路径。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private void setParam(LicenseCreatorParamDTO param) {
// 获取配置文件
Properties prop = new Properties();
try {
prop.load(LicenseCreator.class.getResourceAsStream("/application.properties"));
} catch (IOException e) {
throw new RuntimeException(e);
}
// 读取配置文件中的参数
this.param.setPrivateAlias(prop.getProperty("license.private-alias"));
this.param.setKeyPass(prop.getProperty("license.key-pass"));
this.param.setStorePass(prop.getProperty("license.store-pass"));
this.param.setLicensePath(Paths.get(LicenseConstant.USER_DIR, prop.getProperty("license.license-path")).toString());
this.param.setPrivateKeysStorePath(Paths.get(LicenseConstant.USER_DIR, prop.getProperty("license.private-keys-store-path")).toString());
this.param.setConsumerType(prop.getProperty("license.consumer-type"));
this.param.setConsumerAmount(Integer.parseInt(prop.getProperty("license.consumer-amount")));

// 扩展校验服务器硬件信息
LicenseCheckModel serverInfos = this.getServerInfos(System.getProperty("os.name"));
param.setLicenseCheckModel(serverInfos);
}

2025 年 2 月 7 日

日志打印,字符串拼接。

证书安装完成后,输出证书有效期。

1
log.info(MessageFormat.format("证书安装成功,证书有效期:{1} - {2}", format.format(result.getNotBefore()), format.format(result.getNotAfter())));

同样的,证书校验完成也是如此。

1
log.info(MessageFormat.format("证书校验成功,证书有效期:{1} - {2}", format.format(result.getNotBefore()), format.format(result.getNotAfter())));

证书安装通过后返回的信息比较有限,没有携带证书附加信息,比如证书描述。

1
log.info("证书描述:" + result.getInfo() + MessageFormat.format("证书安装成功,证书有效期:{0} - {1}", format.format(result.getNotBefore()), format.format(result.getNotAfter())));

使用String.format统一下。

1
2
3
4
log.info(String.format("证书安装成功,证书描述:%s,证书有效期:%s - %s",
result.getInfo(),
format.format(result.getNotBefore()),
format.format(result.getNotAfter())));

读取配置

2024 年 12 月 24 日

1
2
3
4
5
6
7
8
// 本地文件路径
Properties prop = new Properties();
try {
prop.load(LicenseCreator.class.getResourceAsStream("/application.properties"));
} catch (IOException e) {
throw new RuntimeException(e);
}
String filePath = Paths.get(LicenseConstant.USER_DIR, prop.getProperty("license.license-path")).toString();
1
2
3
4
5
6
7
8
# license
license.private-alias=zuiyuPrivateKey
license.key-pass=zuiyu_private_password_1234
license.store-pass=zuiyu_public_password_1234
license.consumer-type=User
license.consumer-amount=1
license.license-path=/src/main/resources/license/license.lic
license.private-keys-store-path=/license/zuiyuPrivateKeys.keystore

文件上传

2024 年 12 月 20 日

很早之前做过这方面的研究,上传文件什么的也还是挺容易,就是时间长了就都统统给忘记了,这东西也没法找时间回顾。

只能是遇着问题了,再回过头来重拾解决经验吧。

springboot中文件上传到本地_springboot上传文件到本地-CSDN博客

1
2
3
4
5
6
7
@PostMapping("/upload")
public boolean upload(MultipartFile image) throws Exception {
log.info("文件上传成功 {}", image);
String originalFilename = image.getOriginalFilename();
image.transferTo(new File("D:\\Project\\tellhow\\license-server\\license\\" + originalFilename));
return true;
}

Postman 发起请求,上传文件:

image-20241220175242382

本机查看指定上传目录,当然上传成功了:

image-20241220175213823

文件下载

2024 年 12 月 24 日

java实现文件下载的几种方法_java文件下载到本地-CSDN博客

java 文件下载 下载指定目录下所有文件_mob64ca12d1a59e的技术博客_51CTO博客

2025 年 1 月 1 日

Java 实现浏览器下载文件及文件预览_java_脚本之家 (jb51.net)

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("*")
.allowCredentials(true)
.allowedHeaders("*")
.maxAge(3600);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@PostMapping("/test")
public void test(HttpServletResponse response) {
// 本地文件路径
Properties prop = new Properties();
try {
prop.load(LicenseCreator.class.getResourceAsStream("/application.properties"));
} catch (IOException e) {
throw new RuntimeException(e);
}
// 下载路径
String filePath = System.getProperty("user.dir") + "/backend-server/src/main/resources/license/new.png";
// 下载时文件名(客户端保存时看到的文件名)
String downloadFileName = "new.png";
try {
// 使用缓冲输入流读取本地文件
BufferedInputStream inputStream = new BufferedInputStream(Files.newInputStream(Paths.get(filePath)));
// 使用HttpServletResponse的输出流将文件发送给客户端
BufferedOutputStream outputStream = new BufferedOutputStream(response.getOutputStream());
// 设置响应头
// response.setContentType("application/octet-stream"); // 设置为通用二进制文件类型
response.setContentType("image/png"); // 根据文件类型设置正确的MIME类型
// response.setContentType("image/png"); // 根据文件类型设置正确的MIME类型
response.setHeader("Content-Disposition", "attachment;filename=" + downloadFileName);
response.addHeader("Pargam","no-cache");
response.addHeader("Cache-Control","no-cache");
// 设置Content-Disposition头部以触发下载
// response.setContentLength((int) new File(filePath).length()); // 可选:设置内容长度,有助于浏览器正确处理下载
// 读取文件内容并写入响应输出流
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
// 关闭流
outputStream.flush();
outputStream.close();
} catch (IOException e) {
throw new RuntimeException("文件下载失败", e);
}
}

image-20250101140820016

解决啦!

image-20250101142630014

后端代码基本清楚了,就是前端代码有些问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<script>
function sendPostRequest() {
// 创建一个JSON对象作为请求体
const requestBody = {
licenseId: "dba9097c-b866-430c-8eb8-15a05f37b4da",
};

// 使用fetch API发送POST请求
fetch("http://localhost:8081/backend-server/license/test", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(requestBody),
})
.then((response) => {
if (!response.ok) {
throw new Error(
"Network response was not ok " + response.statusText
);
}
return response.json(); // 解析JSON响应
})
.then((data) => {
console.log("Success:", data); // 在控制台打印成功响应
alert(
"Request sent successfully! Check the console for the response."
);
})
.catch((error) => {
console.error(
"There was a problem with your fetch operation:",
error
); // 在控制台打印错误
// alert("There was a problem with your request. Please try again.");
});
}
</script>

在前端代码中,您正在使用 fetch API 发送一个 POST 请求,并期望返回一个 JSON 响应。然而,根据您的后端设置,您实际上是在发送一个文件作为响应体,而不是 JSON。因此,当前的前端代码不会触发文件下载,而是会尝试解析一个它认为是 JSON 的响应体,这会导致错误。

要修改前端代码以触发文件下载,您不能使用 fetch API 的默认行为,因为 fetch 不会自动处理文件下载(特别是当 Content-Disposition 设置为 attachment 时)。相反,您可以使用一个传统的表单提交,或者通过创建一个临时的 <a> 标签并设置其 href 属性为一个包含文件数据的 Blob URL 来模拟下载。

但是,由于您正在发送一个 POST 请求,并且需要包含请求体,使用表单提交可能不太方便(除非您可以将数据编码为表单字段)。因此,我将提供一个使用 <a> 标签和 Blob 的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<script>
function sendPostRequestToDownload() {
// 创建一个FormData对象来存储请求体数据
const formData = new FormData();
formData.append("licenseId", "dba9097c-b866-430c-8eb8-15a05f37b4da");

// 创建一个XMLHttpRequest对象
const xhr = new XMLHttpRequest();
xhr.open(
"POST",
"http://localhost:8081/backend-server/license/test",
true
);

// 设置正确的响应类型(对于Blob是必要的)
xhr.responseType = "blob";

// 当请求完成时调用此函数
xhr.onload = function () {
if (xhr.status === 200) {
// 创建一个Blob对象,表示下载的文件内容
const blob = xhr.response;

// 创建一个指向Blob对象的URL
const url = URL.createObjectURL(blob);

// 创建一个<a>标签,设置其href属性为Blob URL,并触发点击事件以下载文件
const a = document.createElement("a");
a.style.display = "none";
a.href = url;
// 设置下载的文件名(从响应头中获取,或者硬编码)
a.download = "new.png"; // 这里假设文件名总是new.png,或者您可以从响应头中提取
document.body.appendChild(a);
a.click();

// 释放URL对象
URL.revokeObjectURL(url);
} else {
console.error(
"Request failed with status " + xhr.status + ": " + xhr.statusText
);
}
};

// 发送请求
xhr.send(formData);
}
</script>
1
2
<!-- 您可以添加一个按钮来触发下载请求 -->
<button onclick="sendPostRequestToDownload()">Download File</button>

在这个例子中,我使用了 XMLHttpRequest 而不是 fetch,因为 XMLHttpRequest 允许我们设置 responseType'blob',这样我们就可以获取到原始的二进制数据,然后创建一个 Blob URL 来下载文件。

请注意,由于跨域请求(CORS)的限制,如果前端和后端不在同一个域上,您可能需要在后端配置 CORS 策略以允许来自前端域的请求。在您的例子中,前端和后端似乎都在本地运行,但使用了不同的端口,因此您可能需要在后端设置适当的 CORS 头部。

呐,这里设置通用的二进制文件响应头吧。

1
2
// 设置响应头
response.setContentType("application/octet-stream"); // 设置为通用二进制文件类型

很显然,使用缓冲输入输出流也没有关系的:

1
2
3
4
5
// 使用缓冲输入流读取本地文件
BufferedInputStream inputStream = new BufferedInputStream(Files.newInputStream(Paths.get(filePath)));
// 使用HttpServletResponse的输出流将文件发送给客户端
// ServletOutputStream outputStream = response.getOutputStream();
BufferedOutputStream outputStream = new BufferedOutputStream(response.getOutputStream());

image-20250101143633327

发送 JSON 数据

如果您需要发送 JSON 数据作为请求体,您可以将 JSON 字符串化,并设置正确的 Content-Type 头部。但是,请注意,不是所有的服务器都期望接收 JSON 格式的 POST 数据,这取决于您的后端如何处理请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function sendJsonPostRequest() {
const requestBody = JSON.stringify({
licenseId: "dba9097c-b866-430c-8eb8-15a05f37b4da"
});

const xhr = new XMLHttpRequest();
xhr.open('POST', 'http://localhost:8081/backend-server/license/test', true);
xhr.setRequestHeader('Content-Type', 'application/json');

xhr.onload = function () {
if (xhr.status === 200) {
// 处理响应,可能是文件下载或其他类型的响应
console.log('Request succeeded:', xhr.responseText);
} else {
console.error('Request failed with status ' + xhr.status + ': ' + xhr.statusText);
}
};

xhr.send(requestBody);
}

发送表单数据

如果您需要发送表单数据(例如键值对),您可以使用 FormData 对象。这种方式更适合于发送文件上传请求或简单的表单提交。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function sendFormDataPostRequest() {
const formData = new FormData();
formData.append('licenseId', 'dba9097c-b866-430c-8eb8-15a05f37b4da');
// 如果需要发送文件,可以使用 formData.append('file', fileInput.files[0]);

const xhr = new XMLHttpRequest();
xhr.open('POST', 'http://localhost:8081/backend-server/license/test', true);

// 注意:不需要设置 Content-Type 头部,因为 XMLHttpRequest 会自动设置
// 如果你设置了,它可能会被覆盖,并且可能导致请求失败

xhr.onload = function () {
if (xhr.status === 200) {
// 处理响应,特别是如果服务器返回了一个文件作为响应
// 在这种情况下,您可能需要创建一个 Blob URL 并触发下载
const blob = xhr.response;
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
// 这里需要设置正确的文件名,可以从响应头中获取,或者与后端约定一个文件名
a.download = 'downloaded_file.ext'; // 替换为实际的文件扩展名
document.body.appendChild(a);
a.click();
URL.revokeObjectURL(url);
} else {
console.error('Request failed with status ' + xhr.status + ': ' + xhr.statusText);
}
};

xhr.send(formData);
}

坑我半天。。

1
2
3
4
5
6
7
8
9
// 创建一个<a>标签,设置其href属性为Blob URL,并触发点击事件以下载文件
const a = document.createElement("a");
a.style.display = "none";
a.href = url;
// 设置下载的文件名(从响应头中获取,或者硬编码)
a.download = formData.licenseId + ".lic"; // 这里假设文件名总是new.png,或者您可以从响应头中提取
// a.download = "new.png"; // 这里假设文件名总是new.png,或者您可以从响应头中提取
document.body.appendChild(a);
a.click();

这么写,设置下载的文件名:

1
2
// 设置下载的文件名(从响应头中获取,或者硬编码)
a.download = formData.get("licenseId") + ".lic";

image-20250101145643334

网络文件

2024 年 12 月 24 日

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* 网络文件下载
*/
@PostMapping("/download_3")
public void doFileDownload_3(HttpServletResponse response) {
// 下载网络文件
int byteRead = 0;
int byteSum = 0;
URLConnection con = null;
try {
// 网络文件路径
URL url = new URL("https://img-home.csdnimg.cn/images/20240218021830.png");
con = url.openConnection();
InputStream inputStream = con.getInputStream();
con.setConnectTimeout(20 * 1000);
// 指定下载路径
File filePath = new File("D:\\Project\\tellhow\\file-download\\src\\main\\resources");
if (!filePath.exists()) filePath.mkdirs();
String filename = "download_3.png";
OutputStream outputStream = Files.newOutputStream(Paths.get(filePath.getPath() + "\\" + filename));
// 读取文件
byte[] buff = new byte[1024];
while ((byteRead = inputStream.read(buff)) >= 0) {
byteSum += byteRead;
System.out.println(byteSum);
outputStream.write(buff, 0, byteRead);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}

本地文件

2024 年 12 月 24 日

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* 本地文件下载
*/
@PostMapping("/download_2")
public void doFileDownload_2(HttpServletResponse response) {
// 下载本地文件
int byteRead = 0;
int byteSum = 0;
try {
// 本地文件路径
FileInputStream inputStream = new FileInputStream(System.getProperty("user.dir") + "/src/main/resources/license/new.png");
// 指定下载路径
File filePath = new File("D:\\Project\\tellhow\\file-download\\src\\main\\resources");
if (!filePath.exists()) filePath.mkdirs();
String filename = "download_2.png";
OutputStream outputStream = Files.newOutputStream(Paths.get(filePath.getPath() + "\\" + filename));
// 读取文件
byte[] buff = new byte[1024];
while ((byteRead = inputStream.read(buff)) >= 0) {
byteSum += byteRead;
System.out.println(byteSum);
outputStream.write(buff, 0, byteRead);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}

image-20241224152833914

image-20241224152846709

客户端下载

2024 年 12 月 24 日

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* 客户端下载
*
* @param response response
* @return
*/
@PostMapping("/download_1")
public HttpServletResponse doFileDownload_1(HttpServletResponse response) {
// 本地文件路径
String filePath = System.getProperty("user.dir") + "/src/main/resources/license/new.png";
// 下载时文件名(客户端保存时看到的文件名)
String downloadFileName = "download_1.png";
try (
// 使用缓冲输入流读取本地文件
BufferedInputStream inputStream = new BufferedInputStream(Files.newInputStream(Paths.get(filePath)));
// 使用HttpServletResponse的输出流将文件发送给客户端
BufferedOutputStream outputStream = new BufferedOutputStream(response.getOutputStream())
) {
// 设置响应头
response.setContentType("image/png"); // 根据文件类型设置正确的MIME类型
response.setHeader("Content-Disposition", "attachment; filename=\"" + downloadFileName + "\"");
// 读取文件内容并写入响应输出流
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
// 刷新输出流以确保所有数据都被发送
outputStream.flush();
} catch (IOException e) {
// 处理异常,例如记录日志或返回错误响应
throw new RuntimeException("文件下载失败", e);
}
return response;
}

定时任务

2024 年 12 月 23 日

写一个定时任务,启动类上添加 @EnableScheduling 注解,表示支持定时任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
@SpringBootApplication
@Import(CorsConfig.class)
@EnableConfigurationProperties(UserPasswordConfigInfo.class)
@ServletComponentScan(basePackages = "edu.hpu.filter")
@EnableScheduling
@EnableFeignClients(basePackages = {"com.th.cloud.iois.feign.api"})
@EnableDiscoveryClient
public class ThIoisInspectionApplication {

public static void main(String[] args) {
SpringApplication.run(ThIoisInspectionApplication.class, args);
}
}

编写定时任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 证书校验定时任务
*/
@Component
public class LicenseVerifyJob {
@Resource
private LicenseVerify licenseVerify;

@Scheduled(cron = "*/2 * * * * *")
public void getArticleContent() {
System.out.println("定时任务执行" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
}
}

image-20241223110409136

日期

2024 年 12 月 23 日

输出当前时间,并设置日期格式:

1
System.out.println("当前时间: " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));

2025 年 1 月 2 日

1
2
3
4
5
6
7
8
9
/**
* 转换时间格式
*/
private Date convertTimeFormat(LocalDateTime localDateTime) {
// 使用系统默认时区将 LocalDateTime 转换为 ZonedDateTime
ZonedDateTime zonedDateTime = localDateTime.atZone(ZoneId.systemDefault());
// 将 ZonedDateTime 转换为 Date
return Date.from(zonedDateTime.toInstant());
}
1
2
3
4
5
6
7
8
9
10
11
/**
* 将 Date 转换为 LocalDateTime,使用系统默认时区
*/
private LocalDateTime convertDateToLocalDateTime(Date date) {
// 将 Date 转换为 Instant
Instant instant = date.toInstant();
// 使用系统默认时区将 Instant 转换为 ZonedDateTime
ZonedDateTime zonedDateTime = instant.atZone(ZoneId.systemDefault());
// 从 ZonedDateTime 中提取 LocalDateTime
return zonedDateTime.toLocalDateTime();
}

日志

2024 年 12 月 23 日

Logback.xml 配置详解_logback.xml配置文件详解-CSDN博客

logback.xml配置详解_logback.xml配置文件详解-CSDN博客

java logback 设置文件 logback配置文件_mob64ca140fd7c1的技术博客_51CTO博客

IDEA控制台不同类型日志显示不同颜色_idea设置控制台输出日志颜色-CSDN博客

在项目 resources 目录下新增 logback.xml 配置文件:

1
2
<property name="log.pattern"
value="%highlight(%contextName-) %red(%d{yyyy-MM-dd HH:mm:ss}) %green([%thread]) %highlight(%-5level) %boldMagenta(%logger:%line) - %gray(%msg%n)"/>

这行代码是一个日志配置属性,用于定义日志的输出格式。它使用了一种特定的语法来指定日志信息中各个部分的显示方式,包括颜色、日期、线程名、日志级别、日志记录器(logger)及其所在行号,以及实际的日志消息。下面是对这个配置属性的详细解释:

  • log.pattern:这是配置属性的名称,表示这是一个用于定义日志输出模式的属性。
  • value:后面跟随的字符串是log.pattern属性的值,即具体的日志格式。
  • %highlight(%contextName-):这部分用于输出日志的上下文名称(context name),并使用高亮显示。如果上下文名称为空,则不显示。
  • %red(%d{yyyy-MM-dd HH:mm:ss}):这部分用于输出日志的时间戳,格式为“年-月-日 时:分:秒”。时间戳将以红色显示。
  • %green([%thread]):这部分用于输出生成日志的线程名称,并将线程名称以绿色显示,且整个线程名称被方括号包围。
  • %highlight(%-5level):这部分用于输出日志级别(如INFO、DEBUG等),并使用高亮显示。%-5表示日志级别左对齐并占用至少5个字符的宽度。
  • %boldMagenta(%logger:%line):这部分用于输出日志记录器的名称和产生日志的代码行号,并以粗体洋红色显示。%logger是日志记录器的名称,%line是日志记录所在的代码行号。
  • %gray(%msg%n):这部分用于输出实际的日志消息,并以灰色显示。%msg代表日志消息本身,%n是换行符。

电网

2024 年 12 月 23 日

国家电网和南方电网如何区分? (qq.com)

(8 封私信 / 80 条消息) 电网相关知识 - 搜索结果 - 知乎 (zhihu.com)

2024 年 12 月 27 日

IEC61850 协议解读_iec61850通讯协议-CSDN博客

2025 年 1 月 10 日

「 深圳市朗驰欣创科技股份有限公司」 (launchdigital.net)

image-20250110140935339

一点多开始看这官网学习了一个多小时,了解到常见的智能巡检设备,智能巡检机器人,智能红外等。

专业的电网数智化转型服务提供商 - 格蒂电力 (grid-elec.com)

变电站远程智能巡视解决方案-华雁智能科技(集团)股份有限公司 (whayer.cn)

大立科技 - 专注红外三十年 - 专业红外热像仪,红外热成像,红外测温仪,人体测温仪厂家 (dali-tech.com)

本机硬件信息

2024 年 12 月 24 日

Windows10使用批处理获取电脑的详细信息并在指定路径保存该信息_批处理获取电脑硬件信息-CSDN博客

1
dxdiag

image-20241224095145404

image-20241224094546232

ANSI是什么编码?_ansi编码-CSDN博客

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
 @echo off
echo 开始获取本机信息,请稍等......

if exist D:\ComputerInfos.txt (
echo ===================本机已删除旧的信息文件,请重新运行获取!!!==================
del D:\ComputerInfos.txt
) else (
rem 查看本机系统详细信息
echo 1-本机系统详细信息>>D:\ComputerInfos.txt
systeminfo>>D:\ComputerInfos.txt
rem 查看本机CPU信息
echo 2-本机CPU序列号>>D:\ComputerInfos.txt
wmic cpu get processorid>>D:\ComputerInfos.txt
rem 2-查看本机主板信息
echo 主板信息(主板厂家、系统序号、主板序列号、主板版本)>>D:\ComputerInfos.txt
wmic baseboard get Manufacturer, Product, SerialNumber, Version>>D:\ComputerInfos.txt
rem 3-查看本机产品信息
echo 本机信息(本机序列号、品牌、型号)>>D:\ComputerInfos.txt
wmic csproduct get IdentifyingNumber,Vendor, Version>>D:\ComputerInfos.txt
rem 4-查看本机BIOS信息
echo 本机BIOS信息(本机BIOS序列号)>>D:\ComputerInfos.txt
wmic bios get serialnumber>>D:\ComputerInfos.txt
rem 查看本机系统信息dxdiag

rem 5-查看本机的所有网络信息
echo 本机所有网络信息>>D:\ComputerInfos.txt
ipconfig /all>>D:\ComputerInfos.txt
echo "===================本机信息保存在 D:\ComputerInfos.txt================ "
)

pause

Win10命令大全:掌握这35个实用命令,轻松解决Windows问题_win10教程_小鱼一键重装系统官网 (xiaoyuxitong.com)

Kubernetes全面概述-腾讯云开发者社区-腾讯云 (tencent.com)

RFC文档:官网、中文RFC文档 及 HTTP/2相关文档_rfc官网-CSDN博客

2025 年 1 月 13 日

获取本机 IP 地址:

1
ipconfig | findstr /R "IPv4.*"

获取本机 MAC 地址:

1
ipconfig /all | findstr /R "物理地址"

获取本机 CPU 序列号:

1
wmic cpu get processorid

获取本机主板序列号:

1
wmic baseboard get serialnumber

编写批处理脚本文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@echo off
set OUTPUT_FILE=D:\OutputFile.txt

echo 开始获取本机信息...

rem 清空或创建输出文件
if exist %OUTPUT_FILE% del %OUTPUT_FILE%

rem 获取本机IP地址
echo 本机IP地址:>> %OUTPUT_FILE%
ipconfig | findstr /R "IPv4.*" >> %OUTPUT_FILE%
echo. >> %OUTPUT_FILE%

rem 获取本机MAC地址
echo 本机MAC地址:>> %OUTPUT_FILE%
ipconfig /all | findstr /R "物理地址" >> %OUTPUT_FILE%
echo. >> %OUTPUT_FILE%

rem 获取本机CPU序列号
echo CPU序列号:>> %OUTPUT_FILE%
for /f "skip=1 tokens=2 delims==" %%i in ('wmic cpu get processorid /value') do echo %%i >> %OUTPUT_FILE%
echo. >> %OUTPUT_FILE%

rem 获取本机主板序列号
echo 主板序列号:>> %OUTPUT_FILE%
for /f "skip=1 tokens=2 delims==" %%i in ('wmic baseboard get serialnumber /value') do echo %%i >> %OUTPUT_FILE%

echo 本机信息获取完成,结果已保存到 %OUTPUT_FILE%

pause

输出执行结果到指定目录下文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
本机IP地址:
自动配置 IPv4 地址 . . . . . . . : 169.254.232.7
IPv4 地址 . . . . . . . . . . . . : 192.168.88.1
IPv4 地址 . . . . . . . . . . . . : 192.168.116.32

本机MAC地址:
物理地址. . . . . . . . . . . . . : 00-FF-17-FC-DA-1C
物理地址. . . . . . . . . . . . . : 80-45-DD-E3-6E-E0
物理地址. . . . . . . . . . . . . : 82-45-DD-E3-6E-DF
物理地址. . . . . . . . . . . . . : 00-50-56-C0-00-01
物理地址. . . . . . . . . . . . . : 00-50-56-C0-00-08
物理地址. . . . . . . . . . . . . : 80-45-DD-E3-6E-DF
物理地址. . . . . . . . . . . . . : 80-45-DD-E3-6E-E3

CPU序列号:
BFEBFBFF000806C1

主板序列号:
YX02JN9N

仅获取 MAC 地址,批处理:

1
2
3
4
5
6
7
8
echo 开始获取本机信息...
@echo off
for /f "tokens=2 delims=:" %%i in ('ipconfig /all ^| findstr /R "物理地址"') do (
for %%j in (%%i) do (
echo %%~j
)
)
pause

输出到文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@echo off
set OUTPUT_FILE=D:\OutputFile.txt

echo 开始获取本机信息...
@echo off
echo 本机MAC地址:>> %OUTPUT_FILE%
for /f "tokens=2 delims=:" %%i in ('ipconfig /all ^| findstr /R "物理地址"') do (
for %%j in (%%i) do (
echo %%~j
)
) >> %OUTPUT_FILE%

echo 本机信息获取完成,结果已保存到 %OUTPUT_FILE%

pause

输出结果:

1
2
3
4
5
6
7
8
本机MAC地址:
00-FF-17-FC-DA-1C
80-45-DD-E3-6E-E0
82-45-DD-E3-6E-DF
00-50-56-C0-00-01
00-50-56-C0-00-08
80-45-DD-E3-6E-DF
80-45-DD-E3-6E-E3

PowerShell:

1
ipconfig /all | Select-String -Pattern "物理地址" | ForEach-Object { $_.Line.Split(":")[1].Trim() }

最终批处理文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@echo off
set OUTPUT_FILE=D:\OutputFile.txt

echo 开始获取本机信息...

rem 清空或创建输出文件
if exist %OUTPUT_FILE% del %OUTPUT_FILE%

rem 获取本机IP地址
echo 本机IP地址:>> %OUTPUT_FILE%
for /f "tokens=2 delims=:" %%i in ('ipconfig /all ^| findstr /R "IPV4.*"') do (
for %%j in (%%i) do (
echo %%~j
)
) >> %OUTPUT_FILE%
echo. >> %OUTPUT_FILE%

rem 获取本机MAC地址
echo 本机MAC地址:>> %OUTPUT_FILE%
for /f "tokens=2 delims=:" %%i in ('ipconfig /all ^| findstr /R "物理地址"') do (
for %%j in (%%i) do (
echo %%~j
)
) >> %OUTPUT_FILE%
echo. >> %OUTPUT_FILE%

rem 获取本机CPU序列号
echo CPU序列号:>> %OUTPUT_FILE%
for /f "skip=1 tokens=2 delims==" %%i in ('wmic cpu get processorid /value') do echo %%i >> %OUTPUT_FILE%
echo. >> %OUTPUT_FILE%

rem 获取本机主板序列号
echo 主板序列号:>> %OUTPUT_FILE%
for /f "skip=1 tokens=2 delims==" %%i in ('wmic baseboard get serialnumber /value') do echo %%i >> %OUTPUT_FILE%

echo 本机信息获取完成,结果已保存到 %OUTPUT_FILE%

pause

输出内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
本机IP地址:
169.254.232.7(首选)
192.168.88.1(首选)
192.168.116.32(首选)

本机MAC地址:
00-FF-17-FC-DA-1C
80-45-DD-E3-6E-E0
82-45-DD-E3-6E-DF
00-50-56-C0-00-01
00-50-56-C0-00-08
80-45-DD-E3-6E-DF
80-45-DD-E3-6E-E3

CPU序列号:
BFEBFBFF000806C1

主板序列号:
YX02JN9N

CMD和Powershell区别 - 知乎 (zhihu.com)

PowerShell 和cmd 的区别 - OSCHINA - 中文开源技术交流社区

cmd powershell 区别 - savagefoo - 博客园 (cnblogs.com)

windows为什么有两个命令行工具?命令提示符与PowerShell有什么区别?_哔哩哔哩_bilibili

cmd 调用 PowerShell 命令:

1
powershell -Command "ipconfig /all | Select-String -Pattern '物理地址' | ForEach-Object { $_.Line.Split(':')[1].Trim() }"

shell脚本实现一键获取linux内存/cpu/磁盘IO信息_linux shell_脚本之家 (jb51.net)

编码

2024 年 12 月 24 日

预备知识:常见的编码 (ASCII, Unicode, UTF-8, GBK, base64, urlencode)-CSDN博客

万字长文讲解编码知识,看这文就够了!-腾讯云开发者社区-腾讯云 (tencent.com)

后台管理

2024 年 12 月 25 日

1
2
3
4
5
<modules>
<module>license-server</module>
<module>project-management</module>
</modules>
<packaging>pom</packaging>
1
java: JDK isn't specified for module 'project-management'

刚分模块完毕,启动模块后出现这样的报错,关闭项目再次重新打开就行了。

2024 年 12 月 26 日

新增 common-server 模块。

根据后端规范,修改下父工程 GVA 坐标:

1
2
3
<groupId>com.memory.cloud</groupId>
<artifactId>iois-backend</artifactId>
<version>0.0.1-SNAPSHOT</version>

还不能直接写项目管理增删改查,先迁移配置到公共模块中。

迁移基本完成,但要实现在 license-server 中调用 user-server 服务实现证书生成前的操作人鉴权,还需要实现远程调用。

微服务架构,引入注册中心 Nacos。

我特么好像明白了,一个简单的后台管理系统不应该整成个微服务的,我需要整合项目管理,用户管理到许可证管理中。

目前为止,根本不需要微服务,暂时分离出 common-server 模块即可,这个项目会越发庞大的,也许吧。

那时候再考虑重构,早着呢。

两点半了,第一阶段项目重构完毕。

image-20241226145159065

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* ClassName: CommonServerApplication
* Package: com.common.server
* Description:
*
* @Author Memory
* @Create 2024/12/26 14:43
* @Version 1.0
*/
@SpringBootApplication
public class CommonServerApplication {
public static void main(String[] args) {
SpringApplication.run(CommonServerApplication.class, args);
}
}

三点整,重构完毕,开始写增删改查。

配置驼峰下划线自动转换(数据库字段是下划线,类属性为驼峰):

1
2
3
4
5
# Mybatis-Plus
mybatis-plus.configuration.map-underscore-to-camel-case=true
mybatis-plus.global-config.db-config.logic-delete-field=is_deleted
mybatis-plus.global-config.db-config.logic-delete-value=0
mybatis-plus.global-config.db-config.logic-not-delete-value=1

这里直接指定驼峰了,怪不得会出错:

1
2
3
4
5
6
7
// 账户不能重复
QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user_account", userAccount);
long count = this.baseMapper.selectCount(queryWrapper);
if (count > 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "账号重复");
}

最后一步,打包,特么老报错。。

1
ERROR] Failed to execute goal on project backend-server: Could not resolve dependencies for project com.backend.server:backend-server:jar:0.0.1-SNAPSHOT: Failed to collect dependencies at com.common.server:common-server:jar:0.0.1-SNAPSHOT: Failed to read artifact descriptor for com.common.server:common-server:jar:0.0.1-SNAPSHOT: Could not find artifact com.backend.cloud:iois-backend:pom:0.0.1-SNAPSHOT -> [Help 1]

image-20241226172438533

关于打包时出现程序包不存在的情况_java: 程序包org.junit.jupiter.api不存在-CSDN博客

特么这就解决了??明天研究。

1
2
3
4
5
6
7
8
9
10
11
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<classifier>exec</classifier>
</configuration>
</plugin>
</plugins>
</build>

启动失败:

image-20241226173903596

在Linux中杀死占用某个端口的进程_linux 杀死端口号进程-CSDN博客

1
netstat -tunlp | grep 8081
1
lsof -i :8081

image-20241226173820565

起来了,起来了。

image-20241226174202802

成功访问后台管理接口文档:后台管理接口文档

image-20241226174922168

2024 年 12 月 30 日

搞懂单点登录SSO,基于SpringBoot+JWT实现单点登录解决方案-腾讯云开发者社区-腾讯云 (tencent.com)

改造用户授权验证为 Token。

1
2
3
// 3. 记录用户的登录态
request.getSession().setAttribute(UserConstant.USER_LOGIN_STATE, userInfo);
return this.getLoginUserVO(userInfo);

导入依赖。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!--jwt-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>

JWTUtils 工具类,生成 Token,校验Token,获取声明 Claims 等。

用户登录,生成 Token,保存并返回。

特么的想起来 Token 保存应该是在 Redis 缓存中的,只好先整合 Redis。

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

配置类 RedisConfig。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Slf4j
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
log.debug("==JedisConnectionFactory==");
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(connectionFactory);
return redisTemplate;
}
}

导入 RedisUtils 类,封装得挺完整:

1
2
3
4
5
6
7
8
9
/**
* 设置指定key的存活时间,单位 秒
*
* @param key
* @param time
*/
public void setExpire(String key, long time) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}

写个测试类吧,连接下服务器 Redis,简单写个增删改查试试看。

1
2
3
# Redis
spring.redis.host=redis.tellhow.com
spring.redis.port=6379

image-20241230095958571

测试代码,模拟用户登录。

1
2
3
4
5
6
7
8
9
@Test
void login() {
redisTemplate.opsForValue().set(String.format(UserConstant.TOKEN__KEY, "12345678", "admin"), "user");
}

@Test
void getLoginUser() {
redisTemplate.opsForValue().get(String.format(UserConstant.TOKEN__KEY, "12345678", "admin"));
}

image-20241230100858589

成功。

用户登录,生成 Token,保存并返回。

1
2
3
4
5
6
7
8
// 3.生成并返回 Token
String token = jwtUtil.createToken(userInfo.getId().toString(), "user");
LoginUserInfoVO loginUserVO = this.getLoginUserVO(userInfo);
Map<String, Object> final_map = new HashMap<>();
final_map.put("token", token);
final_map.put("user", loginUserVO);
redisTemplate.opsForValue().set(String.format(UserConstant.TOKEN__KEY, userInfo.getId(), userInfo.getUserRole()), token);
return final_map;

image-20241230102838953

image-20241230102949088

相当成功。

Token 验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
* token 认证过滤器,任何请求访问服务器都会先被这里拦截验证token合法性
*
* @param request
* @param response
* @param filterChain
* @throws ServletException
* @throws IOException
*/
@Override
protected void doFilterInternal(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader("Authorization");
if (!StringUtils.hasText(token) || !token.startsWith("Bearer ")) {
// 通过开放接口过滤器后,如果没有可解析的 token 就放行
filterChain.doFilter(request, response);
return;
}
// 解析 token
token = token.substring(7);
boolean verifyToken = jwtUtil.verifyToken(token);
if (!verifyToken) {
log.error("当前token已过期");
response.addHeader("message", "not login"); // 设置响应头信息,给前端判断用
response.setStatus(403);
return;
}
// 从 redis 中获取 token
String userId = JwtUtil.getSubjectFromToken(token);
String role = JwtUtil.getClaimFromToken(token, "role");
UserInfo userInfo = redisUtil.getObject(String.format(UserConstant.TOKEN__KEY, userId, role), UserInfo.class);

if (userInfo == null) {
log.error("用户未登录");
response.addHeader("message", "not login"); // 设置响应头信息,给前端判断用
response.setStatus(403);
ThrowUtils.throwIf(!verifyToken, ErrorCode.NOT_LOGIN_ERROR, "用户未登录");
return;
}
// 放行
filterChain.doFilter(request, response);
}

得添加个路径黑白名单校验,登陆注册接口调用直接放行。

1
2
3
4
5
/**
* 访问白名单路径
*/
String USER_LOGIN_WHITE_PATH = "/backend-server/user/login";
String USER_REGISTER_WHITE_PATH = "/backend-server/user/register";
1
2
3
4
5
6
7
8
9
10
// 假设这些路径不需要 token 验证
private final Set<String> whiteListPaths = new HashSet<>();

@Override
protected void initFilterBean() throws ServletException {
// 初始化白名单路径,可以从配置文件中读取
whiteListPaths.add(UserConstant.USER_LOGIN_WHITE_PATH); // 登录路径
whiteListPaths.add(UserConstant.USER_REGISTER_WHITE_PATH); // 注册路径
// 可以添加更多路径
}
1
2
3
4
5
6
String requestURI = request.getRequestURI();
// 检查请求路径是否在白名单中
if (whiteListPaths.contains(requestURI)) {
filterChain.doFilter(request, response);
return;
}

image-20241230103940643

我想明白了。

是的,Token 通常是由前端传输过来的。在用户成功登录后,后端会生成一个 JWT Token 并返回给前端。

前端需要保存这个 Token(通常保存在浏览器的 LocalStorage、SessionStorage 或者 Cookies 中),并在后续的每个请求中携带这个 Token。

这个过滤器放行请求不太合理,就应该设置黑白名单,比如注册登录请求直接放行,其余调用都必须携带Token

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Override
protected void doFilterInternal(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain) throws ServletException, IOException {
String requestURI = request.getRequestURI();
// 检查请求路径是否在白名单中
if (whiteListPaths.contains(requestURI)) {
filterChain.doFilter(request, response);
return;
}

// 解析 token
String token = request.getHeader("Authorization");
token = token.substring(7);
boolean verifyToken = jwtUtil.verifyToken(token);
if (!verifyToken) {
log.error("当前token已过期");
response.addHeader("message", "not login"); // 设置响应头信息,给前端判断用
response.setStatus(403);
return;
}
// 从 redis 中获取 token
String userId = JwtUtil.getSubjectFromToken(token);
String role = JwtUtil.getClaimFromToken(token, "role");
UserInfo userInfo = redisUtil.getObject(String.format(UserConstant.TOKEN__KEY, userId, role), UserInfo.class);

if (userInfo == null) {
log.error("用户未登录");
response.addHeader("message", "not login"); // 设置响应头信息,给前端判断用
response.setStatus(403);
ThrowUtils.throwIf(!verifyToken, ErrorCode.NOT_LOGIN_ERROR, "用户未登录");
return;
}
// 放行
filterChain.doFilter(request, response);
}

生成 License 证书以及下载 License 证书需要的用户鉴权,当前登录用户是否存在在过滤器层面已经校验完成,仅需进一步校验用户身份。

用 AOP。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 生成 License 证书
*
* @return boolean
*/
public LicenseContentVO generateLicense(LicenseCreatorParamRequest param, HttpServletRequest request) {
// 1.用户权限

// 2.校验并设置证书生成参数
Boolean validated = this.validateAndSetParam(param);
ThrowUtils.throwIf(!validated, ErrorCode.PARAMS_ERROR, "证书生成参数校验失败");
// 3.生成并保存证书
LicenseContentVO licenseContentVO = this.generate();
ThrowUtils.throwIf(ObjectUtils.isEmpty(licenseContentVO), ErrorCode.OPERATION_ERROR, "证书生成失败");
return licenseContentVO;
}

至于获取当前登录用户,当然需要根据 Token 获取了。

做了比较大胆的决定,选择设置白名单放行获取登录用户请求,在请求内部解析 Token 获取当前登录用户,可能会有些问题,至少代码冗余了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 获取当前登录用户
*
* @param request
* @return
*/
@Override
public UserInfo getLoginUser(HttpServletRequest request) {
// 先判断是否已登录
// 解析 token
String token = request.getHeader("Authorization");
token = token.substring(7);
boolean verifyToken = jwtUtil.verifyToken(token);
if (!verifyToken) {
log.error("当前token已过期");
return null;
}
// 从redis中获取 token
String userId = JwtUtil.getSubjectFromToken(token);
ThrowUtils.throwIf(StringUtils.isBlank(userId), ErrorCode.NOT_LOGIN_ERROR, "未登录");

UserInfo currentUserInfo = this.getById(userId);
ThrowUtils.throwIf(currentUserInfo.getId() == null || currentUserInfo.getId() == null, ErrorCode.NOT_LOGIN_ERROR);
return currentUserInfo;
}

写成这样了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 获取当前登录用户
*
* @param request
* @return
*/
@Override
public UserInfo getLoginUser(HttpServletRequest request) {
// 先判断是否已登录
String token = request.getHeader("Authorization");
token = token.substring(7);
boolean verifyToken = jwtUtil.verifyToken(token);
if (!verifyToken) {
log.error("当前token已过期");
return null;
}
// 从redis中获取 token
String userId = JwtUtil.getSubjectFromToken(token);
ThrowUtils.throwIf(StringUtils.isBlank(userId), ErrorCode.NOT_LOGIN_ERROR, "未登录");

UserInfo currentUserInfo = this.getById(userId);
ThrowUtils.throwIf(currentUserInfo.getId() == null || currentUserInfo.getId() == null, ErrorCode.NOT_LOGIN_ERROR);
return currentUserInfo;
}

LicenseList -> LicenseListVO:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public Page<LicenseInfoVO> getLicenseInfoPage(LicenseInfoQueryRequest request) {
QueryWrapper<LicenseInfo> queryWrapper = new QueryWrapper<>();
Page<LicenseInfo> licenseInfoPage = this.page(new Page<>(request.getCurrent(), request.getSize()), queryWrapper);

List<LicenseInfo> licenseInfoList = licenseInfoPage.getRecords();
List<LicenseInfoVO> licenseInfoVOList = this.getLicenseInfoVO(licenseInfoList);

Page<LicenseInfoVO> licenseInfoVOPage = new Page<>();
licenseInfoVOPage.setRecords(licenseInfoVOList);
licenseInfoVOPage.setTotal(licenseInfoList.size());
return licenseInfoVOPage;
}
1
2
3
‌开启过滤器后Swagger文档为空的原因及解决方法‌:

‌过滤器配置问题‌:过滤器可能会影响Swagger文档的生成。例如,过滤器可能会修改请求或响应的内容,导致Swagger无法正确解析接口信息。可以尝试调整过滤器的配置,或者暂时禁用过滤器,查看Swagger文档是否恢复正常。

这里为什么会有两次请求,接口文档第二次请求资源路径为 /swagger-resourse。

image-20241230135804867

不止一次。

1
2
3
4
/backend-server/doc.html
/backend-server/swagger-resources/configuration/ui
/backend-server/swagger-resources
/backend-server/v2/api-docs

访问接口文档就会处理这么四个请求,需要全部按白名单处理。

能不能在访问 Swagger 接口文档时统一添加前缀呢。

Swagger访问路径添加前缀_swagger prefix-CSDN博客

暂时添加路径到白名单吧,就用 HashSet。

1
2
3
4
5
6
7
8
9
10
/**
* 访问白名单路径
*/
String USER_LOGIN_WHITE_PATH = "/backend-server/user/login";
String USER_REGISTER_WHITE_PATH = "/backend-server/user/register";
String USER_GET_LOGIN_WHITE_PATH = "/backend-server/user/get/login";
String SWAGGER_DOC_WHITE_PATH = "/backend-server/doc.html";
String SWAGGER_RESOURCES_WHITE_PATH = "/backend-server/swagger-resources";
String SWAGGER_RESOURCES_CONFIGURATION_UI_WHITE_PATH = "/backend-server/swagger-resources/configuration/ui";
String SWAGGER_API_DOCS_WHITE_PATH = "/backend-server/v2/api-docs";
1
2
3
4
5
6
7
8
9
10
11
12
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// 初始化白名单路径,可以从配置文件中读取
whiteListPaths.add(UserConstant.USER_LOGIN_WHITE_PATH); // 登录路径
whiteListPaths.add(UserConstant.USER_REGISTER_WHITE_PATH); // 注册路径
whiteListPaths.add(UserConstant.USER_GET_LOGIN_WHITE_PATH); // 当前登录用户
whiteListPaths.add(UserConstant.SWAGGER_DOC_WHITE_PATH); // Swagger UI文档页面
whiteListPaths.add(UserConstant.SWAGGER_RESOURCES_WHITE_PATH); // Swagger资源路径
whiteListPaths.add(UserConstant.SWAGGER_RESOURCES_CONFIGURATION_UI_WHITE_PATH); // Swagger UI配置资源路径
whiteListPaths.add(UserConstant.SWAGGER_API_DOCS_WHITE_PATH); // Swagger API文档JSON资源路径
// 可以添加更多路径
}

特么服务端怎么出这么大幺蛾子,不知道又在加载什么路径,总之过滤器代码没有限制住请求,该放行的没有放行。

image-20241230164332560

linux 系统清理缓存垃圾_linux清理缓存的方法-CSDN博客

如何查看本地redis的ip_mob649e816aeef7的技术博客_51CTO博客

1
2
3
4
5
if (requestURI.startsWith("/backend-server/webjars")) {
// 放行请求
chain.doFilter(request, response);
return;
}

过滤器,返回响应:

1
2
3
4
5
6
7
8
9
10
11
12
13
if (StringUtils.isBlank(token)) {
log.error("token为空");
httpResponse.addHeader("message", "token为空");
httpResponse.setContentType("application/json");
httpResponse.setCharacterEncoding("UTF-8");
try (PrintWriter out = httpResponse.getWriter()) {
out.write("{\"error\": \"token为空\"}");
} catch (IOException e) {
log.error("Failed to write response", e);
}
httpResponse.setStatus(401);
return;
}

image-20241230171124511

1
2
3
4
5
6
7
log.error("token为空");
// 获取响应输出流
try (PrintWriter out = httpResponse.getWriter()) {
// 序列化响应对象为 JSON 字符串并写入输出流
out.write(gson.toJson(ResultUtils.error(401, "token为空")));
}
return;

这么写还是有点丑,直接这么写就好了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (StringUtils.isBlank(token)) {
log.error("token为空");
httpResponse.addHeader("message", "token为空");
httpResponse.setContentType("application/json");
httpResponse.setCharacterEncoding("UTF-8");
try (PrintWriter out = httpResponse.getWriter()) {
out.write("{\"code\": \"401\"}");
out.write("{\"data\": \"null\"}");
out.write("{\"message\": \"token为空\"}");
} catch (IOException e) {
log.error("Failed to write response", e);
}
httpResponse.setStatus(401);
return;
}

特奶奶的,写错了。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (StringUtils.isBlank(token)) {
log.error("token为空");
httpResponse.addHeader("message", "token为空");
httpResponse.setContentType("application/json");
httpResponse.setCharacterEncoding("UTF-8");
// 创建 JSON 响应体
String jsonResponse = "{\"code\": \"401\", \"data\": null, \"message\": \"Token 为空\"}";
try (PrintWriter out = httpResponse.getWriter()) {
out.write(jsonResponse);
} catch (IOException e) {
log.error("Failed to write response", e);
}
httpResponse.setStatus(401);
return;
}

image-20241230173025786

这样就好多了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (!verifyToken) {
log.error("当前token无效或已过期");
httpResponse.addHeader("message", "当前token无效或已过期");
httpResponse.setContentType("application/json");
httpResponse.setCharacterEncoding("UTF-8");
httpResponse.setStatus(401);
// 创建 JSON 响应体
String jsonResponse = "{\"code\": \"401\", \"data\": null, \"message\": \"当前token无效或已过期\"}";
try (PrintWriter out = httpResponse.getWriter()) {
out.write(jsonResponse);
} catch (IOException e) {
log.error("Failed to write response", e);
}
return;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (ObjectUtils.isEmpty(object)) {
log.error("用户未登录");
httpResponse.addHeader("message", "用户未登录");
httpResponse.setContentType("application/json");
httpResponse.setCharacterEncoding("UTF-8");
httpResponse.setStatus(401);
// 创建 JSON 响应体
String jsonResponse = "{\"code\": \"401\", \"data\": null, \"message\": \"用户未登录\"}";
try (PrintWriter out = httpResponse.getWriter()) {
out.write(jsonResponse);
} catch (IOException e) {
log.error("Failed to write response", e);
}
return;
}

明天抽象这块儿代码,还得注意对各个接口做入参校验,给前端返回尽可能准确的响应信息。

2024 年 12 月 31 日

正则表达式可视化-Visual Regexp:在线测试、学习、构建正则表达式 (wangwl.net)

1
2
3
4
5
6
// IPv4地址的正则表达式
private static final String IPV4_REGEX =
"^([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])" +
"(\\.([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])){3}$";

private static final Pattern IPV4_PATTERN = Pattern.compile(IPV4_REGEX);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 检查所属项目是否为空
String projectId = param.getProjectId();
ThrowUtils.throwIf(StringUtils.isBlank(projectId), ErrorCode.PARAMS_ERROR, "项目 ID 不能为空");
ThrowUtils.throwIf(projectId.length() != 32, ErrorCode.PARAMS_ERROR, "项目 ID 长度必须为 32 位");
// 检查申请人姓名是否为空
ThrowUtils.throwIf(StringUtils.isBlank(param.getApplicantName()), ErrorCode.PARAMS_ERROR, "申请人姓名不能为空");
ThrowUtils.throwIf(param.getApplicantName().length() > 10, ErrorCode.PARAMS_ERROR, "申请人姓名长度不能超过 10 个字符");
// 检查证书描述是否为空
ThrowUtils.throwIf(StringUtils.isBlank(param.getDescription()), ErrorCode.PARAMS_ERROR, "证书描述不能为空");
ThrowUtils.throwIf(param.getDescription().length() > 50, ErrorCode.PARAMS_ERROR, "证书描述长度不能超过 50 个字符");
// 检查证书有效期天数是否为空
this.validityDays = param.getValidityDays();
ThrowUtils.throwIf(ObjectUtils.isEmpty(this.validityDays), ErrorCode.PARAMS_ERROR, "证书有效期天数不能为空");
ThrowUtils.throwIf(this.validityDays <= 0, ErrorCode.PARAMS_ERROR, "证书有效期天数必须大于 0");
ThrowUtils.throwIf(this.validityDays > 365, ErrorCode.PARAMS_ERROR, "证书有效期天数不能超过 365 天");
// 计算生效时间和失效时间
LocalDateTime issuedTime = LocalDateTime.now();
LocalDateTime expirationTime = issuedTime.plusDays(this.validityDays);
// 检查失效时间是否早于或等于当前时间
ThrowUtils.throwIf(expirationTime.isBefore(issuedTime) || expirationTime.isEqual(issuedTime), ErrorCode.PARAMS_ERROR, "证书失效时间不能早于或等于生效时间");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
LicenseCheckModel licenseCheckModel = param.getLicenseCheckModel();
// 检查服务器硬件信息是否为空
ThrowUtils.throwIf(ObjectUtils.isEmpty(licenseCheckModel), ErrorCode.PARAMS_ERROR, "服务器硬件信息不能为空");
List<String> ipAddress = licenseCheckModel.getIpAddress();
List<String> macAddress = licenseCheckModel.getMacAddress();
String cpuSerial = licenseCheckModel.getCpuSerial();
String mainBoardSerial = licenseCheckModel.getMainBoardSerial();

ThrowUtils.throwIf(ObjectUtils.isEmpty(cpuSerial), ErrorCode.PARAMS_ERROR, "CPU序列号不能为空");
ThrowUtils.throwIf(ObjectUtils.isEmpty(mainBoardSerial), ErrorCode.PARAMS_ERROR, "主板序列号不能为空");
ThrowUtils.throwIf(ObjectUtils.isEmpty(ipAddress), ErrorCode.PARAMS_ERROR, "可允许的IP地址不能为空");
ThrowUtils.throwIf(ObjectUtils.isEmpty(macAddress), ErrorCode.PARAMS_ERROR, "可允许的MAC地址不能为空");

ThrowUtils.throwIf(!ipAddress.stream().allMatch(ip -> IPV4_PATTERN.matcher(ip).matches()), ErrorCode.PARAMS_ERROR, "IP地址格式错误");
ThrowUtils.throwIf(!macAddress.stream().allMatch(mac -> MAC_PATTERN.matcher(mac).matches()), ErrorCode.PARAMS_ERROR, "MAC地址格式错误");
ThrowUtils.throwIf(!MAC_PATTERN.matcher(cpuSerial).matches(), ErrorCode.PARAMS_ERROR, "CPU序列号格式错误");
ThrowUtils.throwIf(!MAC_PATTERN.matcher(mainBoardSerial).matches(), ErrorCode.PARAMS_ERROR, "主板序列号格式错误");

跑不出来,只能暂时关闭 token 校验,再优化下用户信息保存。

1
2
3
4
5
6
7
8
9
// 3. 记录用户的登录态
request.getSession().setAttribute(UserConstant.USER_LOGIN_STATE, userInfo);
// 4.生成并返回 Token
String token = jwtUtil.createToken(userInfo.getId().toString(), userInfo.getUserRole());
LoginUserInfoVO loginUserVO = this.getLoginUserVO(userInfo);
Map<String, Object> final_map = new HashMap<>();
final_map.put("token", token);
final_map.put("user", loginUserVO);
return final_map;

双管齐下,同时使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 获取当前登录用户
*
* @param request
* @return
*/
@Override
public UserInfo getLoginUser(HttpServletRequest request) {
// 先判断是否已登录
UserInfo userInfo = (UserInfo) request.getSession().getAttribute(UserConstant.USER_LOGIN_STATE);
ThrowUtils.throwIf(ObjectUtils.isEmpty(userInfo), ErrorCode.NOT_LOGIN_ERROR, "用户未登录");
return this.getById(userInfo.getId());
}
1
2
3
4
5
英特尔处理器序列号位数
英特尔处理器的序列号通常是一个16位的数字序列,用于唯一标识每个处理器。这个序列号通常位于处理器表面的标签上,可以通过英特尔的官方工具或软件进行查询和验证‌

AMD处理器序列号位数
AMD处理器的序列号也是一个16位的数字序列,通常位于处理器表面的标签上。用户可以通过AMD的官方工具或软件来查询和验证序列号‌

这个正则真折磨人。。

1
2
3
4
5
6
7
// CPU序列号(或主板序列号)的正则表达式(宽松匹配)
// 假设序列号由字母、数字、连字符、点和空格组成,长度在10到40个字符之间
private static final String CPU_SERIAL_REGEX = "^[0-9A-Fa-f]{8}$";
// 主板序列号的正则表达式(字母和数字,长度可变)
private static final String MAINBOARD_SERIAL_REGEX = "^[A-Za-z0-9]{8,}$";
private static final Pattern CPU_SERIAL_PATTERN = Pattern.compile(CPU_SERIAL_REGEX);
private static final Pattern MAINBOARD_SERIAL_PATTERN = Pattern.compile(MAINBOARD_SERIAL_REGEX);

正则基本就搞定了:

获取证书列表,参数校验,json字符串转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
public Page<LicenseInfoVO> getLicenseInfoPage(LicenseInfoQueryRequest request) {
long current = request.getCurrent();
long size = request.getSize();
// 非空
ThrowUtils.throwIf(current < 0 || size < 0, ErrorCode.PARAMS_ERROR, "分页条件有误");
// 分页查询
Page<LicenseInfo> licenseInfoPage = this.page(new Page<>(request.getCurrent(), request.getSize()),
getQueryWrapper(request));
// 转换
List<LicenseInfo> licenseInfoList = licenseInfoPage.getRecords();
List<LicenseInfoVO> licenseInfoVOList = this.getLicenseInfoVO(licenseInfoList);
// 返回
Page<LicenseInfoVO> licenseInfoVOPage = new Page<>();
licenseInfoVOPage.setRecords(licenseInfoVOList);
licenseInfoVOPage.setTotal(licenseInfoList.size());
return licenseInfoVOPage;
}

这一步转换很关键。

类型推断:

  • 在使用 Gson.fromJson() 方法时,由于 List.class 是原始类型(raw type),它不会提供关于列表中元素类型的具体信息。这可能导致编译器无法正确推断出 fromJson() 方法的返回类型,尽管在实际运行时它通常会返回一个 List<String>。为了解决这个问题,您应该使用 TypeToken 来指定具体的类型参数,如之前所示。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private List<LicenseInfoVO> getLicenseInfoVO(List<LicenseInfo> licenseInfoList) {
Gson gson = new Gson();
// 转换
return licenseInfoList.stream().map(licenseInfo -> {
LicenseInfoVO licenseInfoVO = new LicenseInfoVO();
BeanUtils.copyProperties(licenseInfo, licenseInfoVO);
// 确保类型安全
Type listType = new TypeToken<List<String>>() {
}.getType();
// 解析JSON字符串
licenseInfoVO.setIpAddress(gson.fromJson(licenseInfo.getIpAddress(), listType));
licenseInfoVO.setMacAddress(gson.fromJson(licenseInfo.getMacAddress(), listType));
return licenseInfoVO;
}).collect(Collectors.toList());
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public QueryWrapper<LicenseInfo> getQueryWrapper(LicenseInfoQueryRequest request) {
if (request == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "获取证书列表所需参数不能为空");
}
String id = request.getId();
String licenseName = request.getLicenseName();
String projectId = request.getProjectId();
Integer validityDays = request.getValidityDays();
String applicantName = request.getApplicantName();
Integer status = request.getStatus();
ThrowUtils.throwIf(status == null, ErrorCode.PARAMS_ERROR, "参数为空");
QueryWrapper<LicenseInfo> queryWrapper = new QueryWrapper<>();
// 拼接查询条件
queryWrapper.eq(id != null, "id", id);
queryWrapper.eq(StringUtils.isNotBlank(projectId) && projectId.length() < 20, "license_name", licenseName);
queryWrapper.eq(StringUtils.isNotBlank(licenseName) && licenseName.length() < 30, "license_name", licenseName);
queryWrapper.eq(StringUtils.isNotBlank(applicantName) && applicantName.length() < 10, "application_name", applicantName);
queryWrapper.eq(!ObjectUtils.isEmpty(validityDays) && validityDays > 0 && validityDays < 365, "validity_days", validityDays);
queryWrapper.eq(!ObjectUtils.isEmpty(status) && !ObjectUtils.isEmpty(LicenseStatusEnum.getEnumByValue(status)), "status", status);
return queryWrapper;
}

需要写一些常量。

1
2
3
4
5
6
7
// 拼接查询条件
queryWrapper.eq(StringUtils.isNotEmpty(id), "id", id);
queryWrapper.eq(StringUtils.isNotEmpty(projectId) && projectId.length() < 20, "license_name", licenseName);
queryWrapper.eq(StringUtils.isNotEmpty(licenseName) && licenseName.length() < 30, "license_name", licenseName);
queryWrapper.eq(StringUtils.isNotEmpty(applicantName) && applicantName.length() < 10, "application_name", applicantName);
queryWrapper.eq(!ObjectUtils.isEmpty(validityDays) && validityDays > 0 && validityDays < 365, "validity_days", validityDays);
queryWrapper.eq(!ObjectUtils.isEmpty(status) && !ObjectUtils.isEmpty(LicenseStatusEnum.getEnumByValue(status)), "status", status);

这里就能看出 blank 和 empty 的区别了,传空字符串””,isNotEmpty 处理会认为这是 null 值,而 isNotBlank 处理认为这是空字符串。

1
2
3
4
5
6
7
// 拼接查询条件
queryWrapper.eq(StringUtils.isNotEmpty(id), "id", id);
queryWrapper.eq(StringUtils.isNotEmpty(projectId) && projectId.length() < 20, "license_name", licenseName);
queryWrapper.like(StringUtils.isNotEmpty(licenseName) && licenseName.length() < 30, "license_name", licenseName);
queryWrapper.like(StringUtils.isNotEmpty(applicantName) && applicantName.length() < 10, "application_name", applicantName);
queryWrapper.eq(!ObjectUtils.isEmpty(validityDays) && validityDays > 0 && validityDays < 365, "validity_days", validityDays);
queryWrapper.eq(!ObjectUtils.isEmpty(status) && !ObjectUtils.isEmpty(LicenseStatusEnum.getEnumByValue(status)), "status", status);

证书信息标准:

1
2
3
4
5
6
7
8
/**
* 证书信息标准
*/
Integer PROJECT_ID = 20; // 所属项目id长度不能超过20字符
Integer LICENSE_NAME = 30; // 证书名长度不能超过30字符
Integer APPLICANT_NAME = 10; // 申请人长度不能超过10字符
Integer VALIDITY_DAYS_MAX = 365; // 许可期限最长为365天
Integer VALIDITY_DAYS_MIN = 7;// 许可期限最短为7天
1
2
3
4
5
6
7
8
// 拼接查询条件
queryWrapper.eq(StringUtils.isNotEmpty(id), "id", id);
queryWrapper.eq(StringUtils.isNotEmpty(projectId) && projectId.length() <= LicenseConstant.PROJECT_ID, "license_name", licenseName);
queryWrapper.like(StringUtils.isNotEmpty(licenseName) && licenseName.length() <= LicenseConstant.LICENSE_NAME, "license_name", licenseName);
queryWrapper.like(StringUtils.isNotEmpty(applicantName) && applicantName.length() <= LicenseConstant.APPLICANT_NAME, "application_name", applicantName);
queryWrapper.eq(!ObjectUtils.isEmpty(validityDays) && validityDays >= LicenseConstant.VALIDITY_DAYS_MIN && validityDays <= LicenseConstant.VALIDITY_DAYS_MAX, "validity_days", validityDays);
queryWrapper.eq(!ObjectUtils.isEmpty(status) && !ObjectUtils.isEmpty(LicenseStatusEnum.getEnumByValue(status)), "status", status);
return queryWrapper;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 检查所属项目是否为空
String projectId = param.getProjectId();
ThrowUtils.throwIf(StringUtils.isBlank(projectId), ErrorCode.PARAMS_ERROR, "所属项目ID不能为空");
ThrowUtils.throwIf(projectId.length() > LicenseConstant.PROJECT_ID, ErrorCode.PARAMS_ERROR, "所属项目ID长度不能超过20个字符");
// 检查申请人姓名是否为空
String applicantName = param.getApplicantName();
ThrowUtils.throwIf(StringUtils.isBlank(applicantName), ErrorCode.PARAMS_ERROR, "申请人姓名不能为空");
ThrowUtils.throwIf(applicantName.length() > LicenseConstant.APPLICANT_NAME, ErrorCode.PARAMS_ERROR, "申请人姓名长度不能超过10个字符");
// 检查证书描述是否为空
String licenseName = param.getLicenseName();
ThrowUtils.throwIf(StringUtils.isBlank(licenseName), ErrorCode.PARAMS_ERROR, "证书名不能为空");
ThrowUtils.throwIf(licenseName.length() > LicenseConstant.LICENSE_NAME, ErrorCode.PARAMS_ERROR, "证书名长度不能超过30个字符");
// 检查证书有效期天数是否为空
this.validityDays = param.getValidityDays();
ThrowUtils.throwIf(ObjectUtils.isEmpty(this.validityDays), ErrorCode.PARAMS_ERROR, "证书有效期天数不能为空");
ThrowUtils.throwIf(this.validityDays < LicenseConstant.VALIDITY_DAYS_MIN, ErrorCode.PARAMS_ERROR, "证书有效期至少为7天");
ThrowUtils.throwIf(this.validityDays > LicenseConstant.VALIDITY_DAYS_MAX, ErrorCode.PARAMS_ERROR, "证书有效期不能超过365天");

证书状态,需要添加证书撤销功能,删除证书;证书过期之后同样应该改变证书状态为已过期。

1
2
3
4
5
6
/**
* 证书状态(0-生效中,1-已过期,2-已撤销)
*/
@ApiModelProperty(value = "证书状态(0-生效中,1-已过期,2-已撤销)", required = true)
private Integer status;

1
2
3
4
5
6
7
8
9
10
11
12
13
@Getter
public enum LicenseStatusEnum {
VALID(0, "生效中"),
EXPIRED(1, "已过期"),
REVOKED(2, "已撤销");

private final int value;
private final String text;

LicenseStatusEnum(int value, String text) {
this.value = value;
this.text = text;
}

看见分页总有些怪怪的,size 不匹配实际接受的参数,原来是 Mybatis-Plus 分页拦截器没有开启:

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class MybatisPlusConfig {
/**
* 新增分页拦截器,并设置数据库类型为mysql
*/
@Bean(name = "mybatisPlusInterceptor")
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}

但许可证分页查询还有些问题。

1
2
3
4
5
6
7
8
9
10
11
12
// 分页查询
Page<LicenseInfo> licenseInfoPage = this.page(new Page<>(current, size),
getQueryWrapper(request));
// 转换
List<LicenseInfo> licenseInfoList = licenseInfoPage.getRecords();
List<LicenseInfoVO> licenseInfoVOList = this.getLicenseInfoVO(licenseInfoList);
// 返回
Page<LicenseInfoVO> licenseInfoVOPage = new Page<>();
BeanUtils.copyProperties(licenseInfoPage, licenseInfoVOPage);
licenseInfoVOPage.setRecords(licenseInfoVOList);
licenseInfoVOPage.setTotal(licenseInfoList.size());
return licenseInfoVOPage;

漂亮,解决了,是因为新的 licenseInfoVOPage 没有完全拷贝 licenseInfoPage 对象的属性,新增这行代码就搞定了:

1
BeanUtils.copyProperties(licenseInfoPage, licenseInfoVOPage);

image-20241231145743559

默认设置 create_time 字段不随修改而更新,这是个问题:

image-20241231150203438

新增默认排序字段:

1
2
3
4
5
/**
* 排序字段
*/
@ApiModelProperty(value = "排序字段", required = false)
private String sortField = "create_time";
1
2
3
4
5
6
7
8
9
// 拼接查询条件
queryWrapper.eq(StringUtils.isNotEmpty(id), "id", id);
queryWrapper.eq(StringUtils.isNotEmpty(projectId) && projectId.length() <= LicenseConstant.PROJECT_ID, "license_name", licenseName);
queryWrapper.like(StringUtils.isNotEmpty(licenseName) && licenseName.length() <= LicenseConstant.LICENSE_NAME, "license_name", licenseName);
queryWrapper.like(StringUtils.isNotEmpty(applicantName) && applicantName.length() <= LicenseConstant.APPLICANT_NAME, "applicant_name", applicantName);
queryWrapper.eq(!ObjectUtils.isEmpty(validityDays) && validityDays >= LicenseConstant.VALIDITY_DAYS_MIN && validityDays <= LicenseConstant.VALIDITY_DAYS_MAX, "validity_days", validityDays);
queryWrapper.eq(!ObjectUtils.isEmpty(status) && !ObjectUtils.isEmpty(LicenseStatusEnum.getEnumByValue(status)), "status", status);
queryWrapper.orderBy(StringUtils.isNotEmpty(sortField), true, sortField);
return queryWrapper;

这么写吧,这样写才不会出错,最原始的办法,最简单的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 排序字段
*/
@ApiModelProperty(value = "排序字段", required = false)
private String sortField;

/**
* 排序方式,true 表示升序,false 表示降序,默认为升序
*/
@ApiModelProperty(value = "排序方式", required = false)
private Boolean sortOrder;

1
2
3
4
5
6
7
8
// 默认排序
String sortField = request.getSortField();
Boolean sortOrder = request.getSortOrder();
// 如果用户没有提供有效的排序字段,则使用默认值
if (StringUtils.isBlank(sortField)) sortField = LicenseConstant.DEFAULT_SORT_FIELD;
// 如果用户没有提供有效的排序方式,则使用默认值
if (sortOrder == null) sortOrder = LicenseConstant.DEFAULT_SORT_ORDER;
queryWrapper.orderBy(StringUtils.isNotEmpty(sortField), sortOrder, sortField);

特么服气了,下载证书,部署到服务器上,连项目根目录路径都变了。。

image-20241231155130798

真不信了。

image-20241231155619287

原来一直以来部署上去的代码证书都生成失败了,下载就更不用说,因为路径的问题。

image-20241231160637823

这里应该要完善成新增目录,没有目录就递归新建目录,这样不论是服务端还是本地都不会有问题。

还是细节不到位。

艹。

image-20241231160957539

保存到数据库的证书序号 UUID 跟文件目录下的证书序号特么不一致?

1
2
// 证书序号
String licenseID = "";

线上操作的所有证书生成都是失败的,数据库保存信息倒没出错,所以序号是一致的,但数据库有的,文件目录下边不一定有。

两端都统一出这个错,下载证书:

image-20241231170916478

1
2
3
4
5
6
7
8
org.springframework.http.converter.HttpMessageNotWritableException: No converter for [class com.common.server.common.BaseResponse] with preset Content-Type 'application/octet-stream'
at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:312) ~[spring-webmvc-5.3.24.jar:5.3.24]
at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.handleReturnValue(RequestResponseBodyMethodProcessor.java:183) ~[spring-webmvc-5.3.24.jar:5.3.24]
at org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite.handleReturnValue(HandlerMethodReturnValueHandlerComposite.java:78) ~[spring-web-5.3.24.jar:5.3.24]
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:135) ~[spring-webmvc-5.3.24.jar:5.3.24]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895) ~[spring-webmvc-5.3.24.jar:5.3.24]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808) ~[spring-webmvc-5.3.24.jar:5.3.24]

org.springframework.http.converter.HttpMessageNotWritableException…解决方法-阿里云开发者社区 (aliyun.com)

image-20241231163407164

1
java.lang.IllegalArgumentException: When allowCredentials is true, allowedOrigins cannot contain the special value "*" since that cannot be set on the "Access-Control-Allow-Origin" response header. To allow credentials to a set of origins, list them explicitly or consider using "allowedOriginPatterns" instead.

搞了半天跨域,还是失败了,但为什么证书下载是这个样子。

image-20241231164507240

1
2
3
4
首先,确保服务器端在响应中正确设置了以下头部信息:

Content-Disposition: 这个头部应该设置为 attachment; filename="filename.ext",其中 "filename.ext" 是你想要下载的文件名。这个头部告诉浏览器这是一个附件,应该触发下载。
Content-Type: 这个头部应该设置为文件的 MIME 类型,例如 application/pdf 对于 PDF 文件,或者 application/octet-stream 对于未知类型的二进制文件。

Java使用流实现浏览器自动下载文件(附前端请求代码)_java图片下载代码实现-CSDN博客

image-20241231170707613

指定下载个图片可以,但证书下载其实也没问题,问题在于如何触发浏览器的下载功能。

java调用浏览器自身的下载_mob64ca12f3f05d的技术博客_51CTO博客

Java 实现浏览器下载文件及文件预览_java_脚本之家 (jb51.net)

2025 年 1 月 1 日

Java 实现浏览器下载文件及文件预览_java_脚本之家 (jb51.net)

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("*")
.allowCredentials(true)
.allowedHeaders("*")
.maxAge(3600);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@PostMapping("/test")
public void test(HttpServletResponse response) {
// 本地文件路径
Properties prop = new Properties();
try {
prop.load(LicenseCreator.class.getResourceAsStream("/application.properties"));
} catch (IOException e) {
throw new RuntimeException(e);
}
// 下载路径
String filePath = System.getProperty("user.dir") + "/backend-server/src/main/resources/license/new.png";
// 下载时文件名(客户端保存时看到的文件名)
String downloadFileName = "new.png";
try {
// 使用缓冲输入流读取本地文件
BufferedInputStream inputStream = new BufferedInputStream(Files.newInputStream(Paths.get(filePath)));
// 使用HttpServletResponse的输出流将文件发送给客户端
BufferedOutputStream outputStream = new BufferedOutputStream(response.getOutputStream());
// 设置响应头
// response.setContentType("application/octet-stream"); // 设置为通用二进制文件类型
response.setContentType("image/png"); // 根据文件类型设置正确的MIME类型
// response.setContentType("image/png"); // 根据文件类型设置正确的MIME类型
response.setHeader("Content-Disposition", "attachment;filename=" + downloadFileName);
response.addHeader("Pargam","no-cache");
response.addHeader("Cache-Control","no-cache");
// 设置Content-Disposition头部以触发下载
// response.setContentLength((int) new File(filePath).length()); // 可选:设置内容长度,有助于浏览器正确处理下载
// 读取文件内容并写入响应输出流
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
// 关闭流
outputStream.flush();
outputStream.close();
} catch (IOException e) {
throw new RuntimeException("文件下载失败", e);
}
}

image-20250101140820016

解决啦!

image-20250101142630014

后端代码基本清楚了,就是前端代码有些问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<script>
function sendPostRequest() {
// 创建一个JSON对象作为请求体
const requestBody = {
licenseId: "dba9097c-b866-430c-8eb8-15a05f37b4da",
};

// 使用fetch API发送POST请求
fetch("http://localhost:8081/backend-server/license/test", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(requestBody),
})
.then((response) => {
if (!response.ok) {
throw new Error(
"Network response was not ok " + response.statusText
);
}
return response.json(); // 解析JSON响应
})
.then((data) => {
console.log("Success:", data); // 在控制台打印成功响应
alert(
"Request sent successfully! Check the console for the response."
);
})
.catch((error) => {
console.error(
"There was a problem with your fetch operation:",
error
); // 在控制台打印错误
// alert("There was a problem with your request. Please try again.");
});
}
</script>

在前端代码中,您正在使用 fetch API 发送一个 POST 请求,并期望返回一个 JSON 响应。然而,根据您的后端设置,您实际上是在发送一个文件作为响应体,而不是 JSON。因此,当前的前端代码不会触发文件下载,而是会尝试解析一个它认为是 JSON 的响应体,这会导致错误。

要修改前端代码以触发文件下载,您不能使用 fetch API 的默认行为,因为 fetch 不会自动处理文件下载(特别是当 Content-Disposition 设置为 attachment 时)。相反,您可以使用一个传统的表单提交,或者通过创建一个临时的 <a> 标签并设置其 href 属性为一个包含文件数据的 Blob URL 来模拟下载。

但是,由于您正在发送一个 POST 请求,并且需要包含请求体,使用表单提交可能不太方便(除非您可以将数据编码为表单字段)。因此,我将提供一个使用 <a> 标签和 Blob 的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<script>
function sendPostRequestToDownload() {
// 创建一个FormData对象来存储请求体数据
const formData = new FormData();
formData.append("licenseId", "dba9097c-b866-430c-8eb8-15a05f37b4da");

// 创建一个XMLHttpRequest对象
const xhr = new XMLHttpRequest();
xhr.open(
"POST",
"http://localhost:8081/backend-server/license/test",
true
);

// 设置正确的响应类型(对于Blob是必要的)
xhr.responseType = "blob";

// 当请求完成时调用此函数
xhr.onload = function () {
if (xhr.status === 200) {
// 创建一个Blob对象,表示下载的文件内容
const blob = xhr.response;

// 创建一个指向Blob对象的URL
const url = URL.createObjectURL(blob);

// 创建一个<a>标签,设置其href属性为Blob URL,并触发点击事件以下载文件
const a = document.createElement("a");
a.style.display = "none";
a.href = url;
// 设置下载的文件名(从响应头中获取,或者硬编码)
a.download = "new.png"; // 这里假设文件名总是new.png,或者您可以从响应头中提取
document.body.appendChild(a);
a.click();

// 释放URL对象
URL.revokeObjectURL(url);
} else {
console.error(
"Request failed with status " + xhr.status + ": " + xhr.statusText
);
}
};

// 发送请求
xhr.send(formData);
}
</script>
1
2
<!-- 您可以添加一个按钮来触发下载请求 -->
<button onclick="sendPostRequestToDownload()">Download File</button>

在这个例子中,我使用了 XMLHttpRequest 而不是 fetch,因为 XMLHttpRequest 允许我们设置 responseType'blob',这样我们就可以获取到原始的二进制数据,然后创建一个 Blob URL 来下载文件。

请注意,由于跨域请求(CORS)的限制,如果前端和后端不在同一个域上,您可能需要在后端配置 CORS 策略以允许来自前端域的请求。在您的例子中,前端和后端似乎都在本地运行,但使用了不同的端口,因此您可能需要在后端设置适当的 CORS 头部。

呐,这里设置通用的二进制文件响应头吧。

1
2
// 设置响应头
response.setContentType("application/octet-stream"); // 设置为通用二进制文件类型

很显然,使用缓冲输入输出流也没有关系的:

1
2
3
4
5
// 使用缓冲输入流读取本地文件
BufferedInputStream inputStream = new BufferedInputStream(Files.newInputStream(Paths.get(filePath)));
// 使用HttpServletResponse的输出流将文件发送给客户端
// ServletOutputStream outputStream = response.getOutputStream();
BufferedOutputStream outputStream = new BufferedOutputStream(response.getOutputStream());

image-20250101143633327

发送 JSON 数据

如果您需要发送 JSON 数据作为请求体,您可以将 JSON 字符串化,并设置正确的 Content-Type 头部。但是,请注意,不是所有的服务器都期望接收 JSON 格式的 POST 数据,这取决于您的后端如何处理请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function sendJsonPostRequest() {
const requestBody = JSON.stringify({
licenseId: "dba9097c-b866-430c-8eb8-15a05f37b4da"
});

const xhr = new XMLHttpRequest();
xhr.open('POST', 'http://localhost:8081/backend-server/license/test', true);
xhr.setRequestHeader('Content-Type', 'application/json');

xhr.onload = function () {
if (xhr.status === 200) {
// 处理响应,可能是文件下载或其他类型的响应
console.log('Request succeeded:', xhr.responseText);
} else {
console.error('Request failed with status ' + xhr.status + ': ' + xhr.statusText);
}
};

xhr.send(requestBody);
}

发送表单数据

如果您需要发送表单数据(例如键值对),您可以使用 FormData 对象。这种方式更适合于发送文件上传请求或简单的表单提交。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function sendFormDataPostRequest() {
const formData = new FormData();
formData.append('licenseId', 'dba9097c-b866-430c-8eb8-15a05f37b4da');
// 如果需要发送文件,可以使用 formData.append('file', fileInput.files[0]);

const xhr = new XMLHttpRequest();
xhr.open('POST', 'http://localhost:8081/backend-server/license/test', true);

// 注意:不需要设置 Content-Type 头部,因为 XMLHttpRequest 会自动设置
// 如果你设置了,它可能会被覆盖,并且可能导致请求失败

xhr.onload = function () {
if (xhr.status === 200) {
// 处理响应,特别是如果服务器返回了一个文件作为响应
// 在这种情况下,您可能需要创建一个 Blob URL 并触发下载
const blob = xhr.response;
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
// 这里需要设置正确的文件名,可以从响应头中获取,或者与后端约定一个文件名
a.download = 'downloaded_file.ext'; // 替换为实际的文件扩展名
document.body.appendChild(a);
a.click();
URL.revokeObjectURL(url);
} else {
console.error('Request failed with status ' + xhr.status + ': ' + xhr.statusText);
}
};

xhr.send(formData);
}

坑我半天。。

1
2
3
4
5
6
7
8
9
// 创建一个<a>标签,设置其href属性为Blob URL,并触发点击事件以下载文件
const a = document.createElement("a");
a.style.display = "none";
a.href = url;
// 设置下载的文件名(从响应头中获取,或者硬编码)
a.download = formData.licenseId + ".lic"; // 这里假设文件名总是new.png,或者您可以从响应头中提取
// a.download = "new.png"; // 这里假设文件名总是new.png,或者您可以从响应头中提取
document.body.appendChild(a);
a.click();

这么写,设置下载的文件名:

1
2
// 设置下载的文件名(从响应头中获取,或者硬编码)
a.download = formData.get("licenseId") + ".lic";

image-20250101145643334

证书撤销,很简单的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 证书撤销请求参数
*/
@Data
@ApiModel(value = "证书撤销请求参数")
public class LicenseRevokeRequest {

/**
* 证书序号
*/
@ApiModelProperty(value = "证书序号", required = true)
private String licenseId;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* 撤销证书
*
* @param licenseRevokeRequest 撤销参数
*/
@ApiOperation(value = "License 证书撤销", notes = "License 证书撤销")
@PostMapping("/revoke")
public BaseResponse<Boolean> downloadLicense(@RequestBody
LicenseRevokeRequest licenseRevokeRequest, HttpServletRequest request) {
// 1.校验 Controller 层参数
// 证书状态
ThrowUtils.throwIf(ObjectUtils.isEmpty(licenseRevokeRequest), ErrorCode.PARAMS_ERROR, "证书序号不能为空");
String licenseId = licenseRevokeRequest.getLicenseId();
ThrowUtils.throwIf(StringUtils.isBlank(licenseId), ErrorCode.PARAMS_ERROR, "证书序号不能为空");
ThrowUtils.throwIf(licenseId.length() != 36, ErrorCode.PARAMS_ERROR, "证书序号长度必须为 36 位");
QueryWrapper<LicenseInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("id", licenseId);
int count = licenseInfoService.count(queryWrapper);
ThrowUtils.throwIf(count == 0, ErrorCode.NOT_FOUND_ERROR, "指定证书不存在");
// 登录用户状态
UserInfo loginUser = userInfoService.getLoginUser(request);
ThrowUtils.throwIf(ObjectUtils.isEmpty(loginUser), ErrorCode.NOT_LOGIN_ERROR, "用户未登录, 无法下载证书");
// 管理员权限
ThrowUtils.throwIf(!UserRoleEnum.ADMIN.getValue().equals(loginUser.getUserRole()), ErrorCode.NO_AUTH_ERROR, "非管理员, 无权限下载证书");
// 2.撤销证书
// 更改证书状态
LicenseInfo licenseInfo = new LicenseInfo();
licenseInfo.setId(licenseId);
licenseInfo.setStatus(LicenseStatusEnum.REVOKED.getValue());
boolean updateById = licenseInfoService.updateById(licenseInfo);
ThrowUtils.throwIf(!updateById, ErrorCode.OPERATION_ERROR, "撤销证书失败, 请稍后重试");
licenseInfo.setStatus(LicenseStatusEnum.REVOKED.getValue());
// 数据库删除
boolean removeById = licenseInfoService.removeById(licenseId);
ThrowUtils.throwIf(!removeById, ErrorCode.OPERATION_ERROR, "撤销证书失败, 请稍后重试");
// 文件目录删除(不需要, 证书可以保留)
// 3.返回结果
return ResultUtils.success(removeById);
}

2025 年 1 月 2 日

1
2
3
4
5
/**
* 所属项目(项目序号)
*/
@ApiModelProperty(value = "所属项目(项目序号)", required = true)
private String projectId;
1
2
3
4
5
6
// 检查所属项目是否为空
String projectId = param.getProjectId();
ThrowUtils.throwIf(StringUtils.isBlank(projectId), ErrorCode.PARAMS_ERROR, "所属项目ID不能为空");
ThrowUtils.throwIf(projectId.length() > LicenseConstant.PROJECT_ID, ErrorCode.PARAMS_ERROR, "所属项目ID长度不能超过20个字符");
int count = projectInfoService.count(new QueryWrapper<ProjectInfo>().eq("id", projectId));
ThrowUtils.throwIf(count == 0, ErrorCode.PARAMS_ERROR, "所属项目不存在");

增删改查,完善项目管理。

全局返回通用对象,新增重载,支持响应成功提示信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 成功
*
* @param data
* @param <T>
* @return
*/
public static <T> BaseResponse<T> success(T data) {
return new BaseResponse<>(0, data, "ok");
}

/**
* 成功返回结果
*
* @param data 获取的数据
* @param message 提示信息
*/
public static <T> BaseResponse<T> success(T data, String message) {
return new BaseResponse<T>(ErrorCode.SUCCESS.getCode(), data, message);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 创建项目
*
* @param projectInfoAddRequest
* @param request
* @return
*/
@ApiOperation(value = "创建项目", notes = "创建项目")
@PostMapping("/add")
public BaseResponse<Long> addProject(@RequestBody ProjectInfoAddRequest projectInfoAddRequest, HttpServletRequest request) {
if (projectInfoAddRequest == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数为空");
}
Long result = projectInfoService.addProject(projectInfoAddRequest);
return ResultUtils.success(result, "创建项目成功");
}

记得改这个:

image-20250102103333872

指定证书不存在:

1
2
3
4
5
6
7
8
9
// 证书下载路径
String filePath = Paths.get(LicenseConstant.USER_DIR, prop.getProperty("license.license-path"), licenseId + ".lic").toString();
Path path = Paths.get(filePath);
// String filePath = System.getProperty("user.dir") + "/backend-server/src/main/resources/license/new.png";
// 检查文件是否存在
ThrowUtils.throwIf(StringUtils.isBlank(filePath) || !Files.exists(path), ErrorCode.PARAMS_ERROR, "指定证书不存在, 证书下载失败");
// 下载时文件名(客户端保存时看到的文件名)
String downloadFileName = licenseId + ".lic";
// String downloadFileName = "new.png";

公共属性字段填充。

项目状态枚举:

1
2
3
4
5
6
7
8
9
10
11
/**
* 项目状态枚举
*/
@Getter
public enum ProjectStatusEnum {
VALID(0, "在建中"),
EXPIRED(1, "已完工"),
REVOKED(2, "已报废");

private final int value;
private final String text;

2025 年 1 月 3 日

推送。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
1:01:31.068: [iois-backend] git -c core.quotepath=false -c log.showSignature=false add --ignore-errors -A -f -- backend-server
11:01:31.193: [iois-backend] git -c core.quotepath=false -c log.showSignature=false commit -F C:\Users\Lenovo\AppData\Local\Temp\git-commit-msg-.txt --
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
(commit or discard the untracked or modified content in submodules)
modified: backend-server (modified content)
Untracked files:
(use "git add <file>..." to include in what will be committed)
.gitignore
.mvn/
common-server/.gitattributes
common-server/.gitignore
common-server/.mvn/
common-server/mvnw
common-server/mvnw.cmd
mvnw
mvnw.cmd
no changes added to commit (use "git add" and/or "git commit -a")
1
2
3
$ git push origin master
remote: [session-ca4fa18d] S0134: Incorrect username or password (access token)
fatal: Authentication failed for 'https://gitee.com/deng-2022/iois-backend-server.git/'

image-20250103111452682

死活推不上去 backend-server 这个目录,结果把远程仓库对应的空目录删除以后,再提交推送就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Lenovo@LAPTOP-5U3S75BI MINGW64 /d/Project/tellhow/iois-backend-server/iois-backend (master)
$ git pull origin master
remote: Enumerating objects: 3, done.
remote: Counting objects: 100% (3/3), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 2 (delta 1), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (2/2), 919 bytes | 183.00 KiB/s, done.
From https://gitee.com/deng-2022/iois-backend-server
* branch master -> FETCH_HEAD
48ca9da..a72af34 master -> origin/master
warning: unable to rmdir 'backend-server': Directory not empty
Updating 48ca9da..a72af34
Fast-forward
backend-server | 1 -
1 file changed, 1 deletion(-)
delete mode 160000 backend-server

image-20250103111916530

iois-backend-server: 项目管理,许可证管理 (gitee.com)

image-20250103113008512

2025 年 1 月 6 日

从第二周周五开始,这个项目服务端已经构建成多模块后台管理系统,之后的客户端申请证书安装及校验优化方案都将在此处记录。

1
2
3
4
5
org.springframework.web.multipart.MaxUploadSizeExceededException: Maximum upload size exceeded; nested exception is java.lang.IllegalStateException: org.apache.tomcat.util.http.fileupload.impl.FileSizeLimitExceededException: The field file exceeds its maximum permitted size of 1048576 bytes.
at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.handleParseFailure(StandardMultipartHttpServletRequest.java:124)
at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.parseRequest(StandardMultipartHttpServletRequest.java:115)
at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.<init>(StandardMultipartHttpServletRequest.java:88)
at org.springframework.web.multipart.support.StandardServletMultipartResolver.resolveMultipart(StandardServletMultipartResolver.java:122)

添加上传文件大小配置:

1
2
spring.servlet.multipart.max-file-size=5MB
spring.servlet.multipart.max-request-size=5MB

证书上传:

1
2
3
4
// License 证书
String LICENSE_FILE_EXTENSION = ".lic";
// 公钥
String PUBLIC_KEY_FILE_EXTENSION = ".keystore";
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* License 证书上传
*
* @param file 文件
* @return Boolean
*/
@Override
public Boolean upload(MultipartFile file) {
// 1.上传文件
String originalFilename = file.getOriginalFilename();
assert originalFilename != null;
// 2. 提取文件后缀名(扩展名)
String fileExtension = "";
int dotIndex = originalFilename.lastIndexOf('.');
if (dotIndex != -1 && dotIndex < originalFilename.length() - 1) {
fileExtension = originalFilename.substring(dotIndex);
}
// 判断文件类型
try {
// 上传证书
if (fileExtension.equals(LicenseConstant.LICENSE_FILE_EXTENSION)) {
file.transferTo(Paths.get(LicenseConstant.USER_DIR, licensePath));
return true;
}
// 上传公钥
if (fileExtension.equals(LicenseConstant.PUBLIC_KEY_FILE_EXTENSION)) {
file.transferTo(Paths.get(LicenseConstant.USER_DIR, publicKeysStorePath));
return true;
}
// 其他文件
} catch (IOException e) {
throw new RuntimeException(e);
}
// 其他文件
throw new CommonException(401, "不支持的文件类型");
}

改写成这样,完整保存上传文件的文件名及后缀:

1
2
3
4
5
6
7
# license
license:
subject: zuiyu_demo
public-alias: zuiyuPublicCert
store-pass: zuiyu_public_password_1234
license-path: /th-iois-file-server/license/
public-keys-store-path: /th-iois-file-server/license/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 判断文件类型
try {
// 上传证书
if (fileExtension.equals(LicenseConstant.LICENSE_FILE_EXTENSION)) {
log.info("上传证书");
file.transferTo(Paths.get(LicenseConstant.USER_DIR, licensePath, originalFilename, fileExtension));
return true;
}
// 上传公钥
if (fileExtension.equals(LicenseConstant.PUBLIC_KEY_FILE_EXTENSION)) {
log.info("上传公钥");
file.transferTo(Paths.get(LicenseConstant.USER_DIR, publicKeysStorePath, originalFilename, fileExtension));
return true;
}
// 其他文件
} catch (IOException e) {
throw new RuntimeException(e);
}

上传文件之前,判断目录是否存在,不存在则创建。

1
2
3
4
5
6
7
8
9
10
11
12
// 提取文件名
String originalFilename = file.getOriginalFilename();
if (originalFilename == null || originalFilename.isEmpty()) {
log.error("文件名为空");
throw new CommonException(401, "文件名不能为空");
}
// 提取文件后缀(扩展名)
String fileExtension = "";
int dotIndex = originalFilename.lastIndexOf('.');
if (dotIndex != -1 && dotIndex < originalFilename.length() - 1) {
fileExtension = originalFilename.substring(dotIndex);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 判断文件类型
// 上传证书
if (fileExtension.equals(LicenseConstant.LICENSE_FILE_EXTENSION)) {
log.info("上传证书");
Path targetPath = Paths.get(LicenseConstant.USER_DIR, licensePath, originalFilename, fileExtension);
if (createDirectories(targetPath.getParent())) {
try {
file.transferTo(targetPath.toFile());
return true;
} catch (IOException e) {
log.error("保存证书文件失败", e);
}
}
}
// 上传公钥
if (fileExtension.equals(LicenseConstant.PUBLIC_KEY_FILE_EXTENSION)) {
log.info("上传公钥");
Path targetPath = Paths.get(LicenseConstant.USER_DIR, publicKeysStorePath, originalFilename, fileExtension);
if (createDirectories(targetPath.getParent())) {
try {
file.transferTo(targetPath.toFile());
return true;
} catch (IOException e) {
log.error("保存公钥文件失败", e);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 创建目录
*
* @param dir 目录
* @return boolean
*/
private boolean createDirectories(Path dir) {
File file = dir.toFile();
if (!file.exists() && !file.mkdirs()) {
log.error("创建目录失败: {}", dir);
return false;
}
return true;
}

诶,项目启动后自动执行安装证书,证书安装失败后怎么项目启动失败了呢。

1
2
3
4
5
6
7
8
/**
* 应用启动后, 执行
*/
@PostConstruct
public void doLicenseInstall() {
// 应用启动后, 执行临时证书安装
licenseVerifyService.install();
}

image-20250106141841886

艹。看见了:

1
2
3
4
5
6
7
8
9
10
// 1. 安装证书
try {
LicenseManager licenseManager = LicenseManagerHolder.getInstance(initLicenseParam(param));
licenseManager.uninstall();
result = licenseManager.install(new File(param.getLicensePath()));
log.info(MessageFormat.format("证书安装成功,证书有效期:{0} - {1}", format.format(result.getNotBefore()), format.format(result.getNotAfter())));
} catch (Exception e) {
log.error("证书安装失败!", e);
System.exit(0);
}

启动成功,不过保存目录搞错了,多加了个文件后缀。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 上传证书
if (fileExtension.equals(LicenseConstant.LICENSE_FILE_EXTENSION)) {
log.info("上传证书");
Path targetPath = Paths.get(LicenseConstant.USER_DIR, licensePath, originalFilename);
if (createDirectories(targetPath.getParent())) {
try {
file.transferTo(targetPath.toFile());
return true;
} catch (IOException e) {
log.error("保存证书文件失败", e);
}
}
}

看起来这次逻辑正常了。

证书安装。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* License 证书安装
*
* @return LicenseContent
*/
@ApiOperation(value = "License 证书安装", notes = "License 证书安装")
@PostMapping("/install")
public CommonResult<LicenseContent> install(@RequestBody LicenseInstallRequest licenseInstallRequest) {
// 1.安装 License 证书
LicenseContent result = licenseVerifyService.install(licenseInstallRequest);
// 2.返回结果
return CommonResult.success(result);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* License 证书安装
*
* @return LicenseContent
*/
@Override
public LicenseContent install(LicenseInstallRequest request) {
// 提取文件全名
String licenseId = request.getLicenseId();
if (licenseId == null || licenseId.isEmpty()) {
log.error("证书序号不能为空");
throw new CommonException(401, "证书序号不能为空");
}
String originalFilename = licenseId + LicenseConstant.LICENSE_FILE_EXTENSION;
// 设置证书安装参数
LicenseVerifyParam param = new LicenseVerifyParam();
param.setSubject(subject);
param.setPublicAlias(publicAlias);
param.setStorePass(storePass);
param.setLicensePath(Paths.get(LicenseConstant.USER_DIR, licensePath, originalFilename).toString());
param.setPublicKeysStorePath(Paths.get(LicenseConstant.USER_DIR, publicKeysStorePath).toString());
// 证书安装
return new LicenseVerify().install(param);
}

同证书生成以后需要指定序号下载证书一样,证书安装也需要在证书上传成功以后指定序号安装证书。

证书序号校验:

1
2
// 证书序号长度
Integer LICENSE_ID_LENGTH = 36;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* License 证书安装
*
* @return LicenseContent
*/
@ApiOperation(value = "License 证书安装", notes = "License 证书安装")
@PostMapping("/install")
public CommonResult<LicenseContent> install(@RequestBody LicenseInstallRequest licenseInstallRequest) {
// 1.校验 Controller 层参数
String licenseId = licenseInstallRequest.getLicenseId();
if (StringUtils.isBlank(licenseId)) {
log.error("证书序号不能为空");
throw new CommonException(401, "证书序号不能为空");
}
if (!LicenseConstant.DEFAULT_LICENSE_FILE.equals(licenseId) && licenseId.length() != LicenseConstant.LICENSE_ID_LENGTH) {
log.error("证书序号不符合规范");
throw new CommonException(401, "证书序号不符合规范");
}
// 2.安装 License 证书
LicenseContent result = licenseVerifyService.install(licenseInstallRequest);
// 3.返回结果
return CommonResult.success(result);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* License 证书安装
*
* @param request 请求
* @return LicenseContent
*/
@Override
public LicenseContent install(LicenseInstallRequest request) {
// 提取文件全名
String licenseId = request.getLicenseId();
String originalFilename = Paths.get(licenseId, LicenseConstant.LICENSE_FILE_EXTENSION).toString();
// 设置证书安装参数
LicenseVerifyParam param = new LicenseVerifyParam();
param.setSubject(subject); // 证书主题
param.setPublicAlias(publicAlias); // 公钥别称
param.setStorePass(storePass);// 访问公钥库的密码
param.setLicensePath(Paths.get(LicenseConstant.USER_DIR, licensePath, originalFilename).toString()); // 证书路径
param.setPublicKeysStorePath(Paths.get(LicenseConstant.USER_DIR, publicKeysStorePath).toString()); // 公钥路径
// 证书安装
return new LicenseVerify().install(param);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* @param param 证书安装需要的参数
* @return LicenseContent
*/
public synchronized LicenseContent install(LicenseVerifyParam param) {
log.info("++++++++ 开始安装证书 ++++++++");
LicenseContent result = null;
DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 1. 安装证书
try {
LicenseManager licenseManager = LicenseManagerHolder.getInstance(initLicenseParam(param));
licenseManager.uninstall();
result = licenseManager.install(new File(param.getLicensePath()));
log.info(MessageFormat.format("证书安装成功,证书有效期:{0} - {1}", format.format(result.getNotBefore()), format.format(result.getNotAfter())));
} catch (Exception e) {
log.error("证书安装失败!", e);
throw new CommonException(401, "证书安装失败");
// System.exit(0);
}
log.info("++++++++ 证书安装结束 ++++++++");
return result;
}

怪了,这怎么拼接成这个样子。

1
2
3
// 提取文件全名
String licenseId = request.getLicenseId();
String originalFilename = Paths.get(licenseId, LicenseConstant.LICENSE_FILE_EXTENSION).toString();

image-20250106154710086

艹,这是个专门拼接文件目录的方法。

简单的拼接字符串还是用简单的 String.join() 吧:

1
2
3
// 提取文件全名
String licenseId = request.getLicenseId();
String originalFilename = String.join("", licenseId, LicenseConstant.LICENSE_FILE_EXTENSION);

这里还有个问题,证书可以上传多个,只能安装一个;公钥只有一个,不能上传多个公钥,所以公钥路径应该直接写死。

1
2
license-path: /th-iois-file-server/license/
public-keys-store-path: /th-iois-file-server/license/publicCert.keystore

对应证书上传的逻辑也要改写:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 上传公钥
if (fileExtension.equals(LicenseConstant.PUBLIC_KEY_FILE_EXTENSION)) {
log.info("上传公钥");
Path targetPath = Paths.get(LicenseConstant.USER_DIR, publicKeysStorePath);
if (createDirectories(targetPath.getParent())) {
try {
file.transferTo(targetPath.toFile());
return true;
} catch (IOException e) {
log.error("保存公钥文件失败", e);
}
}
}

上传公钥这里,校验完毕文件类型后,直接在指定目录下保存为固定名称公钥文件:publicCert.keystore

没有上传公钥文件,当然是安装失败的,要添加报错提示,公钥文件和对应证书文件都必须提前上传成功。

执行安装之前校验,报错就有迹可循。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Path licensePath = Paths.get(LicenseConstant.USER_DIR, this.licensePath, originalFilename);
// 判断证书是否存在
if (Files.notExists(licensePath)) {
log.error("指定证书不存在, 请先上传证书");
throw new CommonException(401, "指定证书不存在, 请先上传证书");
}
param.setLicensePath(licensePath.toString()); // 证书路径
Path publicKeyStorePath = Paths.get(LicenseConstant.USER_DIR, publicKeysStorePath);
// 判断公钥是否存在
if (Files.notExists(publicKeyStorePath)) {
log.error("公钥不存在, 请先上传公钥");
throw new CommonException(401, "公钥不存在, 请先上传公钥");
}
param.setPublicKeysStorePath(publicKeyStorePath.toString()); // 公钥路径
return new LicenseVerify().install(param);

image-20250106161807610

证书安装前校验证书和公钥是否存在,同样的证书生成前也要校验私钥是否存在,证书和公钥下载前也需要校验是否存在。

公钥下载,证书安装,证书校验。

1
2
3
4
5
6
7
8
9
/**
* 下载公钥
*/
@ApiOperation(value = "公钥下载", notes = "公钥下载")
@PostMapping("/public/key/download")
public void downloadLicense(HttpServletRequest request, HttpServletResponse response) {
// 公钥下载
licenseDownloadService.downloadLicense(request, response);
}

公钥开放,可直接点击下载,这里的逻辑同证书下载基本一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 公钥下载路径
String filePath = Paths.get(LicenseConstant.USER_DIR, prop.getProperty("license.public-keys-store-path")).toString();
Path path = Paths.get(filePath);
// String filePath = System.getProperty("user.dir") + "/backend-server/src/main/resources/license/new.png";
// 检查公钥是否存在
ThrowUtils.throwIf(StringUtils.isBlank(filePath) || !Files.exists(path), ErrorCode.PARAMS_ERROR, "指定证书不存在, 证书下载失败");
// 下载时公钥名(客户端保存时看到的文件名, 这里固定写死)
String downloadFileName = "publicCert.keystore";
try {
// 使用缓冲输入流读取本地公钥
BufferedInputStream inputStream = new BufferedInputStream(Files.newInputStream(path));
// 使用HttpServletResponse的输出流将公钥发送给客户端
BufferedOutputStream outputStream = new BufferedOutputStream(response.getOutputStream());
// 设置响应头
response.setContentType("application/octet-stream"); // 设置为通用二进制文件类型
response.setHeader("Content-Disposition", "attachment; filename=\"" + downloadFileName + "\"");
// 设置Content-Disposition头部以触发下载
response.setContentLength((int) new File(filePath).length()); // 可选:设置内容长度,有助于浏览器正确处理下载
// 读取公钥内容并写入响应输出流
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
// 关闭流
outputStream.flush();
outputStream.close();
} catch (IOException e) {
throw new RuntimeException("公钥下载失败", e);
}

证书生成时的校验,指定生成证书后存放目录,检查私钥是否存在,这一步就完善了生成证书时私钥不存在的提示信息。

1
2
3
4
5
6
7
8
9
10
// 指定生成证书后存放目录
this.param.setLicensePath(Paths.get(LicenseConstant.USER_DIR, prop.getProperty("license.license-path"), licenseID + ".lic").toString());
// 检查私钥是否存在
Path privateKeyStorePath = Paths.get(LicenseConstant.USER_DIR, prop.getProperty("license.private-keys-store-path"));
if (Files.notExists(privateKeyStorePath)) {
log.error("私钥不存在, 请先上传私钥");
throw new BusinessException(ErrorCode.OPERATION_ERROR, "私钥不存在, 请先上传私钥");
}
// 指定私钥存放目录
this.param.setPrivateKeysStorePath(privateKeyStorePath.toString());

在证书生成参数成功指定之后,真正生成证书之前,当然需要检查生成证书后的存放路径是否存在:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 3. 准备证书存储路径
File licenseFile = new File(this.param.getLicensePath());
File parentDir = licenseFile.getParentFile();
// 如果父目录不存在,则创建父目录
if (!parentDir.exists() && !parentDir.mkdirs()) {
log.error("无法创建证书存储目录:{}", parentDir.getAbsolutePath());
throw new BusinessException(ErrorCode.OPERATION_ERROR, "证书存储目录创建失败");
}
try {
licenseManager.store(licenseContent, new File(this.param.getLicensePath()));
log.info("证书生成成功");
log.info("证书路径:{}", this.param.getLicensePath());
} catch (Exception e) {
log.error(MessageFormat.format("证书生成失败:{0}", param), e.getMessage());
}

新的问题,证书恢复。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 2.撤销证书
// 更改证书状态
LicenseInfo licenseInfo = new LicenseInfo();
licenseInfo.setId(licenseId);
licenseInfo.setStatus(LicenseStatusEnum.REVOKED.getValue());
boolean updateById = licenseInfoService.updateById(licenseInfo);
ThrowUtils.throwIf(!updateById, ErrorCode.OPERATION_ERROR, "撤销证书失败, 请稍后重试");
// 数据库删除(暂时不执行删除)
// boolean removeById = licenseInfoService.removeById(licenseId);
// ThrowUtils.throwIf(!removeById, ErrorCode.OPERATION_ERROR, "撤销证书失败, 请稍后重试");
// 文件目录删除(不需要, 证书可以保留)
// 3.返回结果
return ResultUtils.success(true, "License 证书撤销成功");
1
2
3
4
5
6
7
8
9
10
11
12
13
// 2.恢复证书
// 更改证书状态
LicenseInfo licenseInfo = new LicenseInfo();
licenseInfo.setId(licenseId);
licenseInfo.setStatus(LicenseStatusEnum.VALID.getValue());
boolean updateById = licenseInfoService.updateById(licenseInfo);
ThrowUtils.throwIf(!updateById, ErrorCode.OPERATION_ERROR, "恢复证书失败, 请稍后重试");
// 数据库删除(暂时不执行删除)
// boolean removeById = licenseInfoService.removeById(licenseId);
// ThrowUtils.throwIf(!removeById, ErrorCode.OPERATION_ERROR, "恢复证书失败, 请稍后重试");
// 文件目录删除(不需要, 证书可以保留)
// 3.返回结果
return ResultUtils.success(true, "License 证书恢复成功");

简单完善下,证书撤销仅仅简单改变证书状态,暂时不涉及数据库及文件删除。

回到客户端,证书校验。

指定安装已上传证书的其中一个证书,校验证书不需要指定证书,校验此刻安装的唯一证书有效期即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 校验 License 证书
*
* @return boolean
*/
public synchronized LicenseContent verify() {
log.info("++++++++ 开始验证证书 ++++++++");
LicenseManager licenseManager = LicenseManagerHolder.getInstance(null);
DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
LicenseContent result = null;
// 执行校验
try {
result = licenseManager.verify();
log.info(MessageFormat.format("证书校验通过,证书有效期:{0} - {1}", format.format(result.getNotBefore()), format.format(result.getNotAfter())));
} catch (Exception e) {
log.error("证书校验失败!", e);
throw new CommonException(401, "证书校验失败");
}
log.info("++++++++ 证书验证结束 ++++++++");
return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class LicenseManagerHolder {

private static volatile LicenseManager LICENSE_MANAGER;

public static LicenseManager getInstance(LicenseParam param) {
if (LICENSE_MANAGER == null) {
synchronized (LicenseManagerHolder.class) {
if (LICENSE_MANAGER == null) {
LICENSE_MANAGER = new CustomLicenseManager(param);
}
}
}
return LICENSE_MANAGER;
}
}

可以看到证书校验使用了封装完善的接口,无需额外参数信息,不需要作太多变化。

待优化:证书上传和公钥上传写成两个接口,分别支持上传证书和公钥,以便后续证书安装和定时校验。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 公钥上传
*
* @return LicenseContent
*/
@ApiOperation(value = "公钥上传", notes = "公钥上传")
@PostMapping("/public/key/upload")
public CommonResult<Boolean> uploadPublicKey(MultipartFile file) {
// 1.校验 Controller 层参数
if (ObjectUtils.isEmpty(file)) {
return CommonResult.failed("公钥上传参数不能为空");
}
// 2.上传 License 证书
Boolean result = licenseVerifyService.uploadLicense(file);
// 3.返回结果
return CommonResult.success(result, "公钥上传成功");
}

单独分离出公钥上传接口,逻辑比较简单,明天优化。

2025 年 1 月 7 日

今天早上继续优化客户端相关代码。

公钥上传。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* License 证书上传
*
* @param file 文件
* @return Boolean
*/
@Override
public Boolean uploadPublicKey(MultipartFile file) {
// 提取文件全名
String originalFilename = file.getOriginalFilename();
if (originalFilename == null || originalFilename.isEmpty()) {
log.error("文件名为空");
throw new CommonException(401, "文件名不能为空");
}
// 提取文件后缀(扩展名)
String fileExtension = "";
int dotIndex = originalFilename.lastIndexOf('.');
if (dotIndex != -1 && dotIndex < originalFilename.length() - 1) {
fileExtension = originalFilename.substring(dotIndex);
}
// 判断文件类型
// 上传公钥
if (fileExtension.equals(LicenseConstant.PUBLIC_KEY_FILE_EXTENSION)) {
log.info("上传公钥");
Path targetPath = Paths.get(LicenseConstant.USER_DIR, publicKeysStorePath);
if (createDirectories(targetPath.getParent())) {
try {
file.transferTo(targetPath.toFile());
return true;
} catch (IOException e) {
log.error("保存公钥文件失败", e);
}
}
}
// 其他文件
log.error("不支持的文件类型");
// 其他文件
throw new CommonException(401, "不支持的文件类型");
}

简单的复制粘贴下证书上传的部分代码,可以考虑封装重复的逻辑。

抽象出提取文件后缀的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 提取文件后缀(扩展名)
*
* @param originalFilename 文件名
*/
private String extractExtension(String originalFilename) {
if (originalFilename == null || originalFilename.isEmpty()) {
log.error("文件名为空");
throw new CommonException(401, "文件名不能为空");
}
// 提取文件后缀(扩展名)
String fileExtension = "";
int dotIndex = originalFilename.lastIndexOf('.');
if (dotIndex != -1 && dotIndex < originalFilename.length() - 1) {
fileExtension = originalFilename.substring(dotIndex);
}
// 返回文件后缀(扩展名)
return fileExtension;
}

Redis 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Slf4j
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
// 创建RedisTemplate对象
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// 设置Key的序列化器
redisTemplate.setKeySerializer(new StringRedisSerializer());
// 设置Value的序列化器
redisTemplate.setValueSerializer(new StringRedisSerializer());
// 设置HashKey的序列化器
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
// 设置HashValue的序列化器
redisTemplate.setHashValueSerializer(new StringRedisSerializer());
// 设置连接工厂
redisTemplate.setConnectionFactory(connectionFactory);
return redisTemplate;
}
}

有好多抛出异常的工具类没写好,适当的业务逻辑处也要记录日志,这以后就是编码习惯了。

原来分页查询后,封装 pageVO 的 total 参数时,只需要在构造函数中传入即可。

1
2
3
4
5
// 分页查询
Page<UserInfo> userPage = userInfoService.page(new Page<>(current, size), new QueryWrapper<>());
Page<UserInfoVO> userVOPage = new Page<>(current, size, userPage.getTotal());
List<UserInfoVO> userInfoVOList = userInfoService.getUserInfoVO(userPage.getRecords());
userVOPage.setRecords(userInfoVOList);

那么最简单的分页查询,则需要手动填充下 total 值,像这样:

1
2
3
// 分页查询
Page<UserInfo> userPage = userInfoService.page(new Page<>(current, size), new QueryWrapper<>());
userPage.setTotal(userPage.getRecords().size());

服务端,添加公钥更新,私钥更新接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 公钥上传
*
* @return 上传结果
*/
@ApiOperation(value = "公钥上传", notes = "公钥上传")
@PostMapping("/public/key/upload")
public BaseResponse<Boolean> uploadPublicKey(MultipartFile file) {
// 参数校验
ThrowUtils.throwIf(file.isEmpty(), ErrorCode.PARAMS_ERROR, "公钥上传参数不能为空");
// 上传公钥
Boolean result = licenseDownloadService.uploadPublicKey(file);
// 返回结果
return ResultUtils.success(result, "公钥上传成功");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 私钥上传
*
* @return 上传结果
*/
@ApiOperation(value = "私钥上传", notes = "私钥上传")
@PostMapping("/public/key/upload")
public BaseResponse<Boolean> uploadPublicKey(MultipartFile file) {
// 参数校验
ThrowUtils.throwIf(file.isEmpty(), ErrorCode.PARAMS_ERROR, "私钥上传参数不能为空");
// 上传公钥
Boolean result = licenseDownloadService.uploadPrivateKey(file);
// 返回结果
return ResultUtils.success(result, "公钥上传成功");
}

公钥私钥上传,完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* 私钥上传
*
* @param file 文件
* @return Boolean
*/
@Override
public Boolean uploadPrivateKey(MultipartFile file) {
// 提取文件全名
String originalFilename = file.getOriginalFilename();
String fileExtension = extractExtension(originalFilename);
// 判断文件类型
// 上传公钥
if (fileExtension.equals(LicenseConstant.PRIVATE_KEY_FILE_EXTENSION)) {
log.info("上传私钥");
// 获取配置文件
Properties prop = new Properties();
try {
prop.load(LicenseCreator.class.getResourceAsStream("/application.properties"));
} catch (IOException e) {
throw new RuntimeException(e);
}
Path targetPath = Paths.get(LicenseConstant.USER_DIR, prop.getProperty("license.private-keys-store-path"));
if (createDirectories(targetPath.getParent())) {
try {
file.transferTo(targetPath.toFile());
return true;
} catch (IOException e) {
log.error("保存私钥文件失败", e);
}
}
}
// 其他文件
log.error("不支持的文件类型");
throw new BusinessException(ErrorCode.PARAMS_ERROR, "不支持的文件类型");
}
1
2
3
4
// 公钥
String PUBLIC_KEY_FILE_EXTENSION = ".keystore";
// 私钥
String PRIVATE_KEY_FILE_EXTENSION = ".keystore";

用户状态,用户身份校验,这块逻辑通用的地方很多:证书生成,证书下载,证书撤销,证书恢复,公钥上传,公钥下载,私钥下载。

1
2
3
4
// 用户权限
UserInfo userInfo = userInfoService.getLoginUser(request);
ThrowUtils.throwIf(ObjectUtils.isEmpty(userInfo), ErrorCode.NOT_LOGIN_ERROR, "用户未登录");
ThrowUtils.throwIf(!userInfo.getUserRole().equals(UserRoleEnum.ADMIN.getValue()), ErrorCode.NO_AUTH_ERROR, "非管理员无权限生成证书");

只要涉及到证书相关操作,都应该限定登录用户为管理员身份。

写个 AOP 吧。

1
2
3
4
5
<!--aop-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 权限校验
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthCheck {

/**
* 必须有某个角色
*
* @return {@link UserRoleEnum}
*/
String mustRole() default "";
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/**
* 权限校验 AOP
*/
@Aspect
@Component
public class AuthInterceptor {

@Resource
private UserInfoService userInfoService;

/**
* 执行拦截
*
* @param joinPoint 连接点
* @param authCheck 权限校验注解
* @return 放行结果
*/
@Around("@annotation(authCheck)")
public Object doInterceptor(ProceedingJoinPoint joinPoint, AuthCheck authCheck) throws Throwable {
String mustRole = authCheck.mustRole();
RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
// 当前登录用户
UserInfo loginUser = userInfoService.getLoginUser(request);
// 必须有该权限才通过
if (StringUtils.isNotBlank(mustRole)) {
UserRoleEnum mustUserRoleEnum = UserRoleEnum.getEnumByValue(mustRole);
if (mustUserRoleEnum == null) {
throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
}
String userRole = loginUser.getUserRole();
// 必须有管理员权限
if (UserRoleEnum.ADMIN.equals(mustUserRoleEnum)) {
if (!mustRole.equals(userRole)) {
throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
}
}
}
// 通过权限校验,放行
return joinPoint.proceed();
}
}

完善下日志打印,异常信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* 执行拦截
*
* @param joinPoint 连接点
* @param authCheck 权限校验注解
* @return 放行结果
*/
@Around("@annotation(authCheck)")
public Object doInterceptor(ProceedingJoinPoint joinPoint, AuthCheck authCheck) throws Throwable {
String mustRole = authCheck.mustRole();
RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
// 当前登录用户
UserInfo loginUserInfo = userInfoService.getLoginUser(request);
log.info("当前登录用户信息为 {}", loginUserInfo);
ThrowUtils.throwIf(ObjectUtils.isEmpty(loginUserInfo), ErrorCode.NOT_LOGIN_ERROR, "用户未登录");
// 必须有该权限才通过
if (StringUtils.isNotBlank(mustRole)) {
UserRoleEnum mustUserRoleEnum = UserRoleEnum.getEnumByValue(mustRole);
if (mustUserRoleEnum == null) {
log.error("必须有该角色,但是 {} 没有对应枚举", mustRole);
throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "权限错误");
}
String userRole = loginUserInfo.getUserRole();
// 必须有管理员权限
if (UserRoleEnum.ADMIN.equals(mustUserRoleEnum)) {
if (!mustRole.equals(userRole)) {
log.error("必须有管理员权限,但是当前登录用户权限为 {}", userRole);
throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "非管理员无权限执行此操作");
}
}
}
// 通过权限校验,放行
return joinPoint.proceed();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 生成 License 证书
*
* @param param 生成证书所需参数
* @return 生成结果
*/
@ApiOperation(value = "License 证书生成", notes = "License 证书生成")
@PostMapping(value = "/generate")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<LicenseContentVO> generateLicense(@RequestBody LicenseCreatorParamRequest param, HttpServletRequest request) {
// 参数校验
ThrowUtils.throwIf(ObjectUtils.isEmpty(param), ErrorCode.PARAMS_ERROR, "生成证书所需参数不能为空");
// 生成证书
LicenseContentVO result = licenseCreateService.generateLicense(param, request);
// 返回结果
return ResultUtils.success(result, "License 证书生成成功");
}

成功了。

image-20250107154334218

测试了一遍后台管理系统,服务端基本没有问题了,推一下代码,重新部署一下。

1
2
3
4
There are test failures.

Please refer to D:\Project\tellhow\iois-backend-server\iois-backend\backend-server\target\surefire-reports for the individual test results.
Please refer to dump files (if any exist) [date].dump, [date]-jvmRun[N].dump and [date].dumpstream.

小问题。

部署成功,服务端功能基本完善。

2025 年 1 月 8 日

最后把客户端优化一遍,抽取公共逻辑。

抽取逻辑不是什么难事,把 service 层抽取至 iois-common 模块下即可。

可供安装的证书列表。

在证书安装之前,需上传有效证书和唯一公钥,这里应该再加一段逻辑,查看可供安装的证书列表,安装证书更加便捷。

1
2
3
4
5
6
7
8
9
10
11
/**
* @return 查询可安装证书列表
*/
@ApiOperation(value = "查询可安装证书列表", notes = "查询可安装证书列表")
@GetMapping("get/installable/list/")
public CommonResult<List<String>> getInstallableLicenseList() {
// 查询
List<String> result = licenseService.getInstallableLicenseList();
// 返回结果
return CommonResult.success(result, "获取到可安装的证书列表");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* 查询可安装证书列表
*
* @return 证书列表
*/
@Override
public List<String> getInstallableLicenseList() {
// 证书序号列表
List<String> licenseList = new ArrayList<>();
// 证书目录
File dir = new File(Paths.get(LicenseConstant.USER_DIR, this.licensePath).toString());
if (!dir.exists() || !dir.isDirectory()) {
log.error("指定的目录不存在或不是一个有效的目录");
throw new CommonException(401, "指定的目录不存在或不是一个有效的目录");
}
// 从文件名中提取证书序号,这里假设文件名就是证书序号加上.lic后缀
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir.toPath(), "*.lic")) { // 假设证书文件以.lic结尾
for (Path entry : stream) {
String fileName = entry.getFileName().toString();
String licenseNumber = fileName.substring(0, fileName.lastIndexOf('.'));
licenseList.add(licenseNumber);
}
} catch (IOException | DirectoryIteratorException e) {
log.error("读取目录时发生错误: {}", e.getMessage());
throw new CommonException(401, "读取目录时发生错误: " + e.getMessage());
}
// 返回证书序号列表
return licenseList;
}

测试通过,拿到指定目录下已上传的的所有可安装 License 证书,就是有些许简陋,不过仅仅返回证书序号即可。

image-20250108112549033

License 证书客户端下各个模块的配置文件,可不要写串了:

1
2
3
4
5
6
7
# license
license:
subject: zuiyu_demo
public-alias: zuiyuPublicCert
store-pass: zuiyu_public_password_1234
license-path: /th-iois-inspection/license/
public-keys-store-path: /th-iois-inspection/license/publicCerts.keystore
1
2
3
4
5
6
7
# license
license:
subject: zuiyu_demo
public-alias: zuiyuPublicCert
store-pass: zuiyu_public_password_1234
license-path: /th-iois-file-server/license/
public-keys-store-path: /th-iois-file-server/license/publicCerts.keystore

可以考虑重新换证书密钥对了,不过对于开发环境下还为时过早,更换密钥对意味着先前生成的所有证书都将即刻作废。

为了近期证书的测试方便,暂且不需要重新生成密钥对。

下午又发现个问题,客户端验证证书竟然没有填充额外信息么,额外信息应该包括本机硬件信息的。

image-20250108150850830

怪了,生成证书这里没有问题,信息确实封装到证书中去了。

image-20250108152649598

尝试安装这封新的证书。

罢了,安装新证书也是同样的效果,证书生成后包含了额外信息,但安装完成以后竟然没有额外信息了,其中一定有环节出问题了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 设置证书生成正文信息
*
* @return 证书正文信息
*/
private LicenseContent initLicenseContent() {
// 设置证书正文信息
LicenseContent licenseContent = new LicenseContent();
licenseContent.setHolder(LicenseConstant.DEFAULT_HOLDER_AND_ISSUER);
licenseContent.setIssuer(LicenseConstant.DEFAULT_HOLDER_AND_ISSUER);
licenseContent.setSubject(this.param.getSubject());
licenseContent.setInfo(this.param.getDescription());
licenseContent.setConsumerType(this.param.getConsumerType());
licenseContent.setConsumerAmount(this.param.getConsumerAmount());
// 转换时间格式
licenseContent.setIssued(this.convertTimeFormat(this.param.getIssuedTime()));
licenseContent.setNotBefore(this.convertTimeFormat(this.param.getIssuedTime()));
licenseContent.setNotAfter(this.convertTimeFormat(this.param.getExpiryTime()));
// 扩展校验服务器硬件信息
licenseContent.setExtra(this.param.getLicenseCheckModel());
return licenseContent;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 校验 License 证书
*
* @return boolean
*/
public synchronized LicenseContent verify() {
log.info("++++++++ 开始验证证书 ++++++++");
LicenseManager licenseManager = LicenseManagerHolder.getInstance(null);
DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
LicenseContent result = null;
// 执行校验
try {
result = licenseManager.verify();
log.info(MessageFormat.format("证书校验通过,证书有效期:{0} - {1}", format.format(result.getNotBefore()), format.format(result.getNotAfter())));
} catch (Exception e) {
log.error("证书校验失败!", e);
throw new CommonException(401, "证书校验失败");
}
log.info("++++++++ 证书验证结束 ++++++++");
return result;
}

调试后才发现,原来证书安装完成后就已经查不到额外信息了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* @param param 证书安装需要的参数
* @return LicenseContent
*/
public synchronized LicenseContent install(LicenseVerifyParam param) {
log.info("++++++++ 开始安装证书 ++++++++");
LicenseContent result = null;
DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 安装证书
try {
LicenseParam licenseParam = initLicenseParam(param);
LicenseManager licenseManager = LicenseManagerHolder.getInstance(licenseParam);
// 删除旧证书
licenseManager.uninstall();
result = licenseManager.install(new File(param.getLicensePath()));
log.info(MessageFormat.format("证书安装成功,证书有效期:{0} - {1}", format.format(result.getNotBefore()), format.format(result.getNotAfter())));
log.info("证书内容:{}", result);
} catch (Exception e) {
log.error("证书安装失败!", e);
throw new CommonException(401, "证书安装失败");
// System.exit(0);
}
log.info("++++++++ 证书安装结束 ++++++++");
return result;
}

手动拖动上传也没有解决问题,要么生成时没有携带硬件信息,要么就是安装时没有绑定到相关硬件信息。

问题出在这里吗:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 复写install方法,其中validate方法调用本类中的validate方法,校验IP地址、Mac地址等其他信息
*
* @param key byte[]
* @return de.schlichtherle.license.LicenseContent
*/
@Override
protected synchronized LicenseContent install(final byte[] key, final LicenseNotary notary) throws Exception {
final GenericCertificate certificate = getPrivacyGuard().key2cert(key);
notary.verify(certificate);
final LicenseContent content = (LicenseContent) this.load(certificate.getEncoded());
this.validate(content);
setLicenseKey(key);
setCertificate(certificate);
return content;
}
1
LicenseContent install = new CustomLicenseManager(licenseParam).install(new File(param.getLicensePath()));

尝试使用复写的 install 方法,同样没能获取到证书携带的硬件信息。

这证书本身就不携带信息吧。

很显然带着。

客户端这边的硬件信息类属性同服务端那边不一致,服务端那边已经把checkIp,checkMac等这些属性字段注掉了,客户端仍然保留。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
* 自定义需要校验的 License 参数
*/
@Data
public class LicenseCheckModel {

/**
* 可被允许的IP地址
*/
// private Boolean checkIp = true;
private List<String> ipAddress;

/**
* 可被允许的 MAC 地址
*/
// private Boolean checkMac = true;
private List<String> macAddress;

/**
* 可被允许的 CPU 序列号
*/
// private Boolean checkCpu = true;
private String cpuSerial;

/**
* 可被允许的主板序列号
*/
// private Boolean checkMainBoard = true;
private String mainBoardSerial;


@Override
public String toString() {
return "LicenseCheckModel{" +
"ipAddress=" + ipAddress +
", macAddress=" + macAddress +
", cpuSerial='" + cpuSerial + '\'' +
", mainBoardSerial='" + mainBoardSerial + '\'' +
'}';
}
}

会不会是这个原因,统一都注掉试试。

重启项目。

还是没有解决。

查询当前已安装证书信息:

当前证书有效期,生效时间,失效时间,

早上客户端启动后,证书校验过程中一直在报这样的错误:

1
2
3
4
5
6
7
8
9
10
11
12
java.lang.ClassNotFoundException: com/backend/server/license/LicenseCheckModel
Continuing ...
java.lang.NullPointerException: target should not be null
Continuing ...
java.lang.IllegalStateException: The outer element does not return value
Continuing ...
java.lang.IllegalStateException: The outer element does not return value
Continuing ...
java.lang.IllegalStateException: The outer element does not return value
Continuing ...
java.lang.IllegalStateException: The outer element does not return value
Continuing ...

image-20250108112040851

2025 年 1 月 9 日

今天估计最后一天优化这玩意儿了,昨天还遗留俩问题拖到下班也没能解决,今天下午看看吧,早上也没啥精力。

2025 年 1 月 13 日

果然上周四是最后一次优化这玩意儿了,周三的俩问题一直拖到这周周一还没解决,证书校验从来就没有成功过。

拿不到全部的证书信息。

LicenseContent 证书内容包含参数:

1
2
3
4
5
6
7
8
9
10
11
private static final long serialVersionUID = 1L;
private X500Principal holder;
private X500Principal issuer;
private String subject;
private Date issued;
private Date notBefore;
private Date notAfter;
private String consumerType;
private int consumerAmount = 1;
private String info;
private Object extra;

想了下,数据库保存证书描述信息,对应证书内容中的证书名,那么证书名是可以重复的,暂时不添加这段逻辑:

1
2
3
// 检查证书描述是否重复
int count1 = this.count(new QueryWrapper<LicenseInfo>().eq("license_name", licenseName));
ThrowUtils.throwIf(count1 > 0, ErrorCode.PARAMS_ERROR, "该证书名已存在, 请更换成别的证书名");

硬件信息,需要序列化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 设置证书生成正文信息
*
* @return 证书正文信息
*/
private LicenseContent initLicenseContent() {
// 设置证书正文信息
LicenseContent licenseContent = new LicenseContent();
licenseContent.setHolder(LicenseConstant.DEFAULT_HOLDER_AND_ISSUER);
licenseContent.setIssuer(LicenseConstant.DEFAULT_HOLDER_AND_ISSUER);
licenseContent.setSubject(this.param.getSubject());
licenseContent.setInfo(this.param.getDescription());
licenseContent.setConsumerType(this.param.getConsumerType());
licenseContent.setConsumerAmount(this.param.getConsumerAmount());
// 转换时间格式
licenseContent.setIssued(this.convertTimeFormat(this.param.getIssuedTime()));
licenseContent.setNotBefore(this.convertTimeFormat(this.param.getIssuedTime()));
licenseContent.setNotAfter(this.convertTimeFormat(this.param.getExpiryTime()));
// 扩展校验服务器硬件信息
licenseContent.setExtra(this.param.getLicenseCheckModel());
return licenseContent;
}

成功了,安装证书时获取到额外信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"code": 0,
"msg": "证书安装成功",
"data": {
"holder": {
"name": "CN=localhost,OU=localhost,O=localhost,L=SH,ST=SH,C=CN",
"encoded": "MGMxCzAJBgNVBAYTAkNOMQswCQYDVQQIEwJTSDELMAkGA1UEBxMCU0gxEjAQBgNVBAoTCWxvY2FsaG9zdDESMBAGA1UECxMJbG9jYWxob3N0MRIwEAYDVQQDEwlsb2NhbGhvc3Q="
},
"issuer": {
"name": "CN=localhost,OU=localhost,O=localhost,L=SH,ST=SH,C=CN",
"encoded": "MGMxCzAJBgNVBAYTAkNOMQswCQYDVQQIEwJTSDELMAkGA1UEBxMCU0gxEjAQBgNVBAoTCWxvY2FsaG9zdDESMBAGA1UECxMJbG9jYWxob3N0MRIwEAYDVQQDEwlsb2NhbGhvc3Q="
},
"subject": "zuiyu_demo",
"issued": "2025-01-13T07:31:57.142+00:00",
"notBefore": "2025-01-13T07:31:57.142+00:00",
"notAfter": "2025-03-20T07:31:57.142+00:00",
"consumerType": "User",
"consumerAmount": 1,
"info": "周一",
"extra": "{\"ipAddress\":[\"192.168.88.1\",\"192.168.116.32\"],\"macAddress\":[\"00-FF-17-FC-DA-1C\",\"80-45-DD-E3-6E-E0\",\"82-45-DD-E3-6E-DF\",\"00-50-56-C0-00-01\",\"00-50-56-C0-00-08\",\"80-45-DD-E3-6E-DF\",\"80-45-DD-E3-6E-E3\"],\"cpuSerial\":\"BFEBFBFF000806C1\",\"mainBoardSerial\":\"YX02JN9N\"}"
},
"success": true
}

保留下最完整的自定义校验硬件信息逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
* 复写validate方法,增加IP地址、Mac地址等其他信息校验
*
* @param content LicenseContent
*/
@Override
protected synchronized void validate(final LicenseContent content)
throws LicenseContentException {
// 1. 首先调用父类的validate方法
super.validate(content);

// 2. 然后校验自定义的License参数
// License中可被允许的参数信息
LicenseCheckModel expectedCheckModel = (LicenseCheckModel) content.getExtra();
if (expectedCheckModel != null) {
// 当前服务器真实的参数信息
LicenseCheckModel serverCheckModel = getServerInfos();

if (serverCheckModel != null) {
// 校验IP地址
if (expectedCheckModel.getCheckIp() && !checkIpAddress(expectedCheckModel.getIpAddress(), serverCheckModel.getIpAddress())) {
throw new LicenseContentException("当前服务器的IP没在授权范围内");
}

// 校验Mac地址
if (expectedCheckModel.getCheckMac() && !checkIpAddress(expectedCheckModel.getMacAddress(), serverCheckModel.getMacAddress())) {
throw new LicenseContentException("当前服务器的Mac地址没在授权范围内");
}

// 校验主板序列号
if (expectedCheckModel.getCheckMainBoard() && !checkSerial(expectedCheckModel.getMainBoardSerial(), serverCheckModel.getMainBoardSerial())) {
throw new LicenseContentException("当前服务器的主板序列号没在授权范围内");
}

// 校验CPU序列号
if (expectedCheckModel.getCheckCpu() && !checkSerial(expectedCheckModel.getCpuSerial(), serverCheckModel.getCpuSerial())) {
throw new LicenseContentException("当前服务器的CPU序列号没在授权范围内");
}
} else {
throw new LicenseContentException("不能获取服务器硬件信息");
}
}
}

我决定不使用该校验逻辑,CustomLicenseManager 这个自定义类复写了很多内置方法,包括证书生成,安装和校验逻辑。

目前不需要。

1

现在的证书安装和校验逻辑不完善。

安装和校验成功与否,核心在于证书主题和密钥对校验,这部分信息校验已经在内置 API 中完成,不然安装一定会失败的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 初始化证书校验参数
*
* @param param License校验类需要的参数
* @return de.schlichtherle.license.LicenseParam
*/
private LicenseParam initLicenseParam(LicenseVerifyParam param) {
// 获取用户偏好
Preferences preferences = Preferences.userNodeForPackage(LicenseVerify.class);
// 配置公钥存储参数
CipherParam cipherParam = new DefaultCipherParam(param.getStorePass());
// 返回证书生成参数
KeyStoreParam publicStoreParam = new CustomKeyStoreParam(LicenseVerify.class,
param.getPublicKeysStorePath(),
param.getPublicAlias(),
param.getStorePass(),
null);
// 返回证书安装参数
return new DefaultLicenseParam(param.getSubject(), preferences, publicStoreParam, cipherParam);
}

最后要进行额外信息校验,即硬件信息校验。

这部分应该单独写一个新接口,专门在执行证书安装之前单独校验现场硬件信息是否匹配,校验通过才能执行下一步证书安装。

那么校验逻辑应该包含什么呢。

同样的,用脚本文件拿取现场硬件信息,将 IP 地址,MAC 地址,CPU 序列号和主板序列号输入,必须要与指定证书包含额外信息完全匹配。

这就是校验逻辑。

服务端新增接口,获取 License 证书内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* 获取证书内容
*
* @param request 请求参数
* @return 证书内容
*/
@Override
public LicenseContentVO getLicenseContent(LicenseContentQueryRequest request) {
// 参数校验
ThrowUtils.throwIf(ObjectUtils.isEmpty(request), ErrorCode.PARAMS_ERROR, "证书序号不能为空");
String licenseId = request.getLicenseId();
ThrowUtils.throwIf(StringUtils.isBlank(licenseId), ErrorCode.PARAMS_ERROR, "证书序号不能为空");
ThrowUtils.throwIf(licenseId.length() != 36, ErrorCode.PARAMS_ERROR, "证书序号长度必须为 36 位");
QueryWrapper<LicenseInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("id", licenseId);
int count = licenseInfoService.count(queryWrapper);
ThrowUtils.throwIf(count == 0, ErrorCode.NOT_FOUND_ERROR, "指定证书不存在");
// 获取证书内容
LicenseManager licenseManager = LicenseManagerHolder.getInstance(null);
DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
LicenseContent result = null;
// 执行校验
try {
result = licenseManager.verify();
log.info(MessageFormat.format("证书校验通过,证书有效期:{0} - {1}", format.format(result.getNotBefore()), format.format(result.getNotAfter())));
} catch (Exception e) {
log.error("获取证书内容失败!", e);
throw new BusinessException(ErrorCode.OPERATION_ERROR, "获取证书内容失败");
}
//
LicenseContentVO licenseContentVO = new LicenseContentVO();
BeanUtils.copyProperties(result, licenseContentVO);
Gson gson = new Gson();
// 反序列化硬件信息
LicenseCheckModel licenseCheckModel = gson.fromJson(result.getExtra().toString(), LicenseCheckModel.class);
licenseContentVO.setExtra(licenseCheckModel);
// 返回结果
return licenseContentVO;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* License 证书内容获取
*
* @param request 查询参数
*/
@ApiOperation(value = "License 证书内容获取", notes = "License 证书内容获取")
@PostMapping(value = "/get/content")
public BaseResponse<LicenseContentVO> getLicenseContent(@RequestBody LicenseContentQueryRequest request) {
// 参数校验
ThrowUtils.throwIf(ObjectUtils.isEmpty(request), ErrorCode.PARAMS_ERROR, "获取证书列表所需参数不能为空");
// 获取证书内容
LicenseContentVO licenseContentVO = licenseCreateService.getLicenseContent(request);
// 返回结果
return ResultUtils.success(licenseContentVO, "获取证书内容成功");
}

其实不过是证书校验罢了,不需要任何传参。

诶?

不对,校验之前必须得安装证书才行。

艹。

是这样的吗,我再考虑考虑。

就是这样的。

生成证书之前要拿到硬件信息,获取证书内容本质上是校验证书,校验证书之前需要执行安装,那就需要服务端整合下客户端相关代码了。

代码整合需要头脑啊,这会儿都快五点了,脑袋乱糟糟的整合失败了可咋整。

慢慢来。

明天巩固学习下同步本机文件到 Linux 服务器,提升下开发效率。

还有 Docker 部署项目的注意事项。

重新安装下虚拟机吧,之前的 ubuntu 怎么看不了 IPv4 地址了。

shell脚本实现一键获取linux内存/cpu/磁盘IO信息_linux shell_脚本之家 (jb51.net)

2025 年 1 月 14 日

证书安装:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* License 证书安装
*
* @param request 请求
* @return LicenseContent
*/
public LicenseContent install(LicenseInstallRequest request) {
// 提取文件全名
String licenseId = request.getLicenseId();
String originalFilename = String.join("", licenseId, LicenseConstant.LICENSE_FILE_EXTENSION);
// 获取配置文件
Properties prop = new Properties();
try {
prop.load(LicenseCreator.class.getResourceAsStream("/application.properties"));
} catch (IOException e) {
throw new RuntimeException(e);
}
// 设置证书安装参数
LicenseVerifyParam param = new LicenseVerifyParam();
param.setSubject(prop.getProperty("license.subject")); // 证书主题
param.setPublicAlias(prop.getProperty("license.public-alias")); // 公钥别称
param.setStorePass(prop.getProperty("license.store-pass"));// 访问公钥库的密码
Path licensePath = Paths.get(LicenseConstant.USER_DIR, prop.getProperty("license.license-path"), originalFilename);
// 判断证书是否存在
if (Files.notExists(licensePath)) {
log.error("指定证书不存在, 请先上传证书");
throw new BusinessException(ErrorCode.PARAMS_ERROR, "指定证书不存在, 请先上传证书");
}
param.setLicensePath(licensePath.toString()); // 证书路径
Path publicKeyStorePath = Paths.get(LicenseConstant.USER_DIR, prop.getProperty("license.public-keys-store-path"));
// 判断公钥是否存在
if (Files.notExists(publicKeyStorePath)) {
log.error("公钥不存在, 请先上传公钥");
throw new BusinessException(ErrorCode.PARAMS_ERROR, "公钥不存在, 请先上传公钥");
}
param.setPublicKeysStorePath(publicKeyStorePath.toString()); // 公钥路径
// 执行安装
return new LicenseVerify().install(param);
}

证书校验:

1
2
3
4
5
6
7
/**
* License 证书校验
*/
public LicenseContent verify() {
// 执行校验
return new LicenseVerify().verify();
}

优化完善了下项目目录,把服务端证书生成,证书安装和证书校验需要的参数全部整合到 common-server 模块下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* 获取证书内容
*
* @param request 请求参数
* @return 证书内容
*/
@Override
public LicenseContentVO getLicenseContent(LicenseContentQueryRequest request) {
// 参数校验
ThrowUtils.throwIf(ObjectUtils.isEmpty(request), ErrorCode.PARAMS_ERROR, "证书序号不能为空");
String licenseId = request.getLicenseId();
ThrowUtils.throwIf(StringUtils.isBlank(licenseId), ErrorCode.PARAMS_ERROR, "证书序号不能为空");
ThrowUtils.throwIf(licenseId.length() != 36, ErrorCode.PARAMS_ERROR, "证书序号长度必须为 36 位");
QueryWrapper<LicenseInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("id", licenseId);
int count = licenseInfoService.count(queryWrapper);
ThrowUtils.throwIf(count == 0, ErrorCode.NOT_FOUND_ERROR, "指定证书不存在");
// 证书安装
LicenseContent install = this.install(new LicenseInstallRequest(licenseId));
ThrowUtils.throwIf(install == null, ErrorCode.OPERATION_ERROR, "证书安装失败");
// 证书校验
LicenseContent result = this.verify();
// 获取证书内容
LicenseContentVO licenseContentVO = new LicenseContentVO();
BeanUtils.copyProperties(result, licenseContentVO);
Gson gson = new Gson();
// 反序列化硬件信息
LicenseCheckModel licenseCheckModel = gson.fromJson(result.getExtra().toString(), LicenseCheckModel.class);
licenseContentVO.setExtra(licenseCheckModel);
// 返回结果
return licenseContentVO;
}

服务端配置:

1
2
3
4
5
6
7
8
9
10
11
# license
license.subject=zuiyu_demo
license.private-alias=zuiyuPrivateKey
license.public-alias=zuiyuPublicCert
license.key-pass=zuiyu_private_password_1234
license.store-pass=zuiyu_public_password_1234
license.consumer-type=User
license.consumer-amount=1
license.license-path=/backend-server/src/main/resources/license/
license.private-keys-store-path=/backend-server/license/privateKeys.keystore
license.public-keys-store-path=/backend-server/license/publicKeys.keystore

艹,数据库连接失败了,是服务器关闭的缘故吗。

现场服务器硬件信息审核。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* 现场服务器硬件信息
*/
@Data
@ApiModel(value = "现场服务器硬件信息", description = "现场服务器硬件信息")
public class LicenseCheckModel {

/**
* 可被允许的IP地址
*/
@ApiModelProperty(value = "可被允许的IP地址", required = true)
// private Boolean checkIp = true;
private List<String> ipAddress;

/**
* 可被允许的 MAC 地址
*/
@ApiModelProperty(value = "可被允许的 MAC 地址", required = true)
// private Boolean checkMac = true;
private List<String> macAddress;

/**
* 可被允许的 CPU 序列号
*/
@ApiModelProperty(value = "可被允许的 CPU 序列号", required = true)
// private Boolean checkCpu = true;
private String cpuSerial;

/**
* 可被允许的主板序列号
*/
@ApiModelProperty(value = "可被允许的主板序列号", required = true)
// private Boolean checkMainBoard = true;
private String mainBoardSerial;


@Override
public String toString() {
return "LicenseCheckModel{" +
"ipAddress=" + ipAddress +
", macAddress=" + macAddress +
", cpuSerial='" + cpuSerial + '\'' +
", mainBoardSerial='" + mainBoardSerial + '\'' +
'}';
}
}

服务端生成证书,封装现场服务器硬件信息

1
2
3
4
// 封装现场服务器硬件信息
Gson gson = new Gson();
log.info("封装现场服务器硬件信息:{}", this.param.getLicenseCheckModel());
licenseContent.setExtra(gson.toJson(this.param.getLicenseCheckModel()));
1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 现场服务器硬件信息审核
*
* @return 现场服务器硬件信息审核
*/
@ApiOperation(value = "现场服务器硬件信息审核", notes = "现场服务器硬件信息审核")
@GetMapping("/check/info")
public CommonResult<Boolean> checkHardwareInfo(@RequestBody LicenseCheckModel licenseCheckModel) {
// 查询
Boolean result = licenseService.checkHardwareInfo(licenseCheckModel);
// 返回结果
return CommonResult.success(result, "现场服务器硬件信息审核通过");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* 现场服务器硬件信息审核
*
* @param checkInfo 现场服务器硬件信息
* @return 审核结果
*/
@Override
public Boolean checkHardwareInfo(LicenseCheckModel checkInfo) {
// 获取证书内容
LicenseContent licenseContent = new LicenseVerify().verify();
Gson gson = new Gson();
// 反序列化硬件信息
LicenseCheckModel hardwareInfo = gson.fromJson(licenseContent.getExtra().toString(), LicenseCheckModel.class);
// 审核
boolean ipCheck = isListContainsAny(hardwareInfo.getIpAddress(), checkInfo.getIpAddress());
if (!ipCheck) {
log.error("IP 地址审核不通过");
throw new CommonException(401, "IP 地址审核不通过");
}
boolean macCheck = isListContainsAny(hardwareInfo.getMacAddress(), checkInfo.getMacAddress());
if (!macCheck) {
log.error("MAC 地址审核不通过");
throw new CommonException(401, "MAC 地址审核不通过");
}
boolean cpuCheck = hardwareInfo.getCpuSerial().equals(checkInfo.getCpuSerial());
if (!cpuCheck) {
log.error("CPU 序列号审核不通过");
throw new CommonException(401, "CPU 序列号审核不通过");
}
boolean mainBoardCheck = hardwareInfo.getMainBoardSerial().equals(checkInfo.getMainBoardSerial());
if (!mainBoardCheck) {
log.error("主板序列号审核不通过");
throw new CommonException(401, "主板序列号审核不通过");
}

return true;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 审核
*
* @param listA 列表A
* @param listB 列表B
* @return 审核结果
*/
private boolean isListContainsAny(List<String> listA, List<String> listB) {
if (listA == null || listA.isEmpty() || listB == null || listB.isEmpty()) {
return false;
}
for (String item : listB) {
if (listA.contains(item)) {
return true;
}
}
return false;
}

为了实现证书快要过期时的告警或提醒功能,就交给前端做吧,后端只负责拿到证书有效期,前端实现将要过期时的弹窗告警。

客户端校验,失败直接退出。

1
2
3
4
5
6
7
8
9
// 执行校验
try {
result = licenseManager.verify();
log.info(MessageFormat.format("证书校验通过,证书有效期:{0} - {1}", format.format(result.getNotBefore()), format.format(result.getNotAfter())));
} catch (Exception e) {
log.error("证书校验失败!", e);
System.exit(0);
throw new CommonException(401, "证书校验失败");
}

基本完善服务端接口。

2025 年 2 月 5 日

下午,部署下后台管理系统。

奶奶的,半分钟不到,又把本地的数据删了。。。搞错数据库了。

重构数据库,保留数据。

测试,简单部署。

这里有个问题:我只需要修改下项目名称,结果项目其他信息未传参数,更新后项目其他信息都为空了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 更新项目
*
* @param projectInfoUpdateRequest 更新请求
* @return 更新结果
*/
@PostMapping("/update")
@ApiOperation(value = "修改项目", notes = "修改项目")
public BaseResponse<Boolean> updateProject(@RequestBody ProjectInfoUpdateRequest projectInfoUpdateRequest) {
// 参数校验
ThrowUtils.throwIf(ObjectUtils.isEmpty(projectInfoUpdateRequest) || projectInfoUpdateRequest.getId() <= 0, ErrorCode.PARAMS_ERROR, "请求参数为空");
ProjectInfo projectInfo = new ProjectInfo();
BeanUtils.copyProperties(projectInfoUpdateRequest, projectInfo);
// 更新项目
boolean result = projectInfoService.updateById(projectInfo);
ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR, "更新失败");
return ResultUtils.success(true, "更新项目成功");
}

那么同样的,更新用户信息也会存在问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 更新用户
*
* @param userInfoUpdateRequest 更新请求
* @return 更新结果
*/
@PostMapping("/update")
@ApiOperation(value = "修改用户", notes = "修改用户")
public BaseResponse<Boolean> updateUser(@RequestBody UserInfoUpdateRequest userInfoUpdateRequest) {
// 参数校验
ThrowUtils.throwIf(userInfoUpdateRequest == null || userInfoUpdateRequest.getId() <= 0, ErrorCode.PARAMS_ERROR, "修改用户信息不能为空");
UserInfo userInfo = new UserInfo();
BeanUtils.copyProperties(userInfoUpdateRequest, userInfo);
// 更新用户
boolean result = userInfoService.updateById(userInfo);
ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR, "用户信息更新失败");
return ResultUtils.success(true, "用户信息更新成功");
}

明天再改吧,之前没有注意到这里会出现问题。

2025 年 2 月 6 日

在这之后应该干掉旧的默认证书,更新新证书为默认证书。

1
2
3
log.info("证书安装成功");
log.info(MessageFormat.format("证书安装成功,证书有效期:{0} - {1}", format.format(result.getNotBefore()), format.format(result.getNotAfter())));
log.info("证书内容:{}", result);

慢着,好像还不能这么直接改,安装证书的接口都已经写死要求传入待核验的现场服务器硬件信息了,那就先测试获取 docker 容器环境变量。

携带环境变量,启动项目。

1
docker run -p 8080:8080 --name helloapp -e APP_NAME="My Awesome App" -e APP_PORT=8080 hello-app:latest

可以这么干,那就更新下接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 测试接口
*/
@GetMapping("/test")
public String test() {
System.out.println("Hello World");
return "Hello World";
}

/**
* 测试接口2
*/
@GetMapping("/test2")
public String test2() {
Map<String, String> getenv = System.getenv();
System.out.println(getenv);
String appName = System.getenv("APP_NAME");
System.out.println(appName);
String appPort = System.getenv("APP_PORT");
System.out.println(appPort);
return "Hello World";
}

出问题了。

怎么访问不到接口呢。

本地测试没问题啊,docker 部署服务启动也是没问题的,难道是打包更新后的代码出问题了吗,再试试。

1
http://8.141.90.145:8080/api/test2

果然是这样,成功了。

image-20250206153451255

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
[root@iZ2ze4hnl6pls28qt4w1ttZ serInfo]# docker run -p 8080:8080 --name helloapp -e APP_NAME="My Awesome App" -e APP_PORT=8080 hello-app:latest

. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.7.7)

2025-02-06 07:30:58.477 INFO 1 --- [ main] org.tellhow.info.hello.HelloApplication : Starting HelloApplication v0.0.1-SNAPSHOT using Java 1.8.0_212 on 9ed306a10b37 with PID 1 (/app/app.jar started by root in /app)
2025-02-06 07:30:58.484 INFO 1 --- [ main] org.tellhow.info.hello.HelloApplication : The following 1 profile is active: "dev"
2025-02-06 07:30:59.972 INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2025-02-06 07:30:59.988 INFO 1 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2025-02-06 07:30:59.988 INFO 1 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.70]
2025-02-06 07:31:00.135 INFO 1 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2025-02-06 07:31:00.135 INFO 1 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1499 ms
2025-02-06 07:31:00.679 INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2025-02-06 07:31:00.694 INFO 1 --- [ main] org.tellhow.info.hello.HelloApplication : Started HelloApplication in 2.989 seconds (JVM running for 3.58)
Hello world!
2025-02-06 07:31:06.094 INFO 1 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2025-02-06 07:31:06.095 INFO 1 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2025-02-06 07:31:06.096 INFO 1 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 1 ms
Hello World
{PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/lib/jvm/java-1.8-openjdk/jre/bin:/usr/lib/jvm/java-1.8-openjdk/bin, HOSTNAME=9ed306a10b37, JAVA_ALPINE_VERSION=8.212.04-r0, LD_LIBRARY_PATH=/usr/lib/jvm/java-1.8-openjdk/jre/lib/amd64/server:/usr/lib/jvm/java-1.8-openjdk/jre/lib/amd64:/usr/lib/jvm/java-1.8-openjdk/jre/../lib/amd64, JAVA_HOME=/usr/lib/jvm/java-1.8-openjdk, APP_NAME=My Awesome App, JAVA_VERSION=8u212, LANG=C.UTF-8, APP_PORT=8080, HOME=/root}
My Awesome App
8080

嘿嘿哈哈,看来通过 Java 代码获取封装在 Docker 容器环境变量中的服务器硬件信息是可取的,开启下一步修改。

1
2
3
4
5
6
Map<String, String> getenv = System.getenv();
System.out.println(getenv);
String appName = System.getenv("APP_NAME");
System.out.println(appName);
String appPort = System.getenv("APP_PORT");
System.out.println(appPort);

这里,需要校验的现场服务器硬件信息不应该以参数形式传递,而应该从容器的环境变量中获取。

1
2
// 设置现场服务器硬件信息
HardwareInfoContext.setHardwareInfo(hardwareInfo);
1
2
3
4
5
6
7
8
// IP 地址环境变量
String IP_ADDRESS_ENV = "IP_ADDRESS";
// Mac 地址环境变量
String MAC_ADDRESS_ENV = "MAC_ADDRESS";
// CPU 序列号环境变量
String CPU_SERIAL_ENV = "CPU_SERIAL";
// 主板序列号环境变量
String MAINBOARD_SERIAL_ENV = "MAINBOARD_SERIAL";

删除掉这些多余的代码,证书安装不需要传参。

1
2
3
4
5
LicenseCheckModel licenseCheckModel = licenseInstallRequest.getLicenseCheckModel();
if (ObjectUtils.isEmpty(licenseCheckModel)) {
log.error("请输入现场服务器硬件信息!");
throw new CommonException(401, "请输入现场服务器硬件信息!");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 证书安装请求参数
*/
@Data
@ApiModel(value = "证书安装请求参数")
public class LicenseInstallRequest {

/**
* 证书序号
*/
@ApiModelProperty(value = "证书序号", required = true)
private String licenseId;

/**
* 额外的服务器硬件校验信息
*/
@ApiModelProperty(value = "额外的服务器硬件校验信息", required = true)
private LicenseCheckModel licenseCheckModel;
}

如此解析环境变量获取现场服务器硬件信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 获取现场服务器硬件信息
// 获取容器环境变量
LicenseCheckModel hardwareInfo = new LicenseCheckModel();
String ipAddressEnv = System.getenv(LicenseConstant.IP_ADDRESS_ENV);
String MacAddressEnv = System.getenv(LicenseConstant.MAC_ADDRESS_ENV);
String CpuSerialEnv = System.getenv(LicenseConstant.CPU_SERIAL_ENV);
String MainBoardSerialEnv = System.getenv(LicenseConstant.MAINBOARD_SERIAL_ENV);
if (ObjectUtils.isNotEmpty(ipAddressEnv)) {
hardwareInfo.setIpAddress(Lists.newArrayList(ipAddressEnv.split(",")));
}
if (ObjectUtils.isNotEmpty(MacAddressEnv)) {
hardwareInfo.setMacAddress(Lists.newArrayList(MacAddressEnv.split(",")));
}
if (ObjectUtils.isNotEmpty(CpuSerialEnv)) {
hardwareInfo.setCpuSerial(CpuSerialEnv);
}
if (ObjectUtils.isNotEmpty(MainBoardSerialEnv)) {
hardwareInfo.setMainBoardSerial(MainBoardSerialEnv);
}
// 现场服务器硬件信息
if (ObjectUtils.isEmpty(hardwareInfo)) {
log.error("请输入现场服务器硬件信息!");
throw new CommonException(401, "请输入现场服务器硬件信息!");
}
// 设置现场服务器硬件信息
HardwareInfoContext.setHardwareInfo(hardwareInfo);

在这之后应该干掉旧的默认证书,更新新证书为默认证书。

1
2
3
log.info("证书安装成功");
log.info(MessageFormat.format("证书安装成功,证书有效期:{0} - {1}", format.format(result.getNotBefore()), format.format(result.getNotAfter())));
log.info("证书内容:{}", result);

2025 年 2 月 7 日

传参。不需要再次读取配置文件信息了。

1
2
3
4
5
6
// 默认证书文件路径
String defaultFileName = String.join("", LicenseConstant.DEFAULT_LICENSE_FILE, LicenseConstant.LICENSE_FILE_EXTENSION);
String defaultLicensePath = Paths.get(LicenseConstant.USER_DIR, this.licensePath, defaultFileName).toString();
param.setDefaultLicensePath(defaultLicensePath); // 默认证书路径
// 执行安装
return new LicenseVerify().install(param);

证书安装成功后执行:

1
2
3
4
5
6
7
8
9
10
11
12
// 安装证书
try {
LicenseParam licenseParam = initLicenseParam(param);
LicenseManager licenseManager = LicenseManagerHolder.getInstance(licenseParam);
// 删除旧证书
licenseManager.uninstall();
result = licenseManager.install(new File(param.getLicensePath()));
// result = new CustomLicenseManager(licenseParam).install(new File(param.getLicensePath()));
log.info("证书安装成功");
log.info(MessageFormat.format("证书安装成功,证书有效期:{0} - {1}", format.format(result.getNotBefore()), format.format(result.getNotAfter())));
log.info("证书内容:{}", result);
changeDefaultLicense(param);
1
2
3
4
5
6
7
// 删除默认证书
String defaultLicensePath = param.getDefaultLicensePath();
try {
Files.deleteIfExists(Paths.get(defaultLicensePath));
} catch (IOException e) {
throw new RuntimeException(e);
}

果真奏效,默认证书成功删除。

更新证书名称。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* 更新默认证书
*
* @return LicenseContent
*/
public Boolean changeDefaultLicense(LicenseVerifyParam param) {
log.info("++++++++ 开始更换默认证书 ++++++++");
// 删除默认证书
Path renamedLicensePath = Paths.get(param.getDefaultLicensePath());
if (Files.exists(renamedLicensePath)) {
try {
Files.deleteIfExists(renamedLicensePath);
log.info("已删除默认证书以准备重命名新证书:{}", renamedLicensePath);
} catch (IOException e) {
log.error("无法删除默认证书以准备重命名新证书:{}", renamedLicensePath, e);
throw new CommonException(401, "默认证书删除失败");
}
}
// 获取新证书的文件路径
String newLicensePath = param.getLicensePath();
// 更换新证书为默认证书, 将新证书重命名为 default.lic
try {
Files.move(Paths.get(newLicensePath), renamedLicensePath, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
log.error("无法重命名新证书:从 {} 到 {}", newLicensePath, renamedLicensePath, e);
throw new RuntimeException(e);
}
String newLicenseFileName = LicenseConstant.DEFAULT_LICENSE_FILE + LicenseConstant.LICENSE_FILE_EXTENSION;
log.info("新证书已重命名为:{}", newLicenseFileName);
log.info("++++++++ 默认证书更换结束 ++++++++");
return false;
}

File.move() 方法有问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 获取新证书的文件路径
String newLicensePath = param.getLicensePath();
// 更换新证书为默认证书, 将新证书重命名为 default.lic
try {
// 重新构建目录
Files.createDirectories(renamedLicensePath);
Files.move(Paths.get(newLicensePath), renamedLicensePath, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
log.error("无法重命名新证书:从 {} 到 {}", newLicensePath, renamedLicensePath, e);
throw new RuntimeException(e);
}
String newLicenseFileName = LicenseConstant.DEFAULT_LICENSE_FILE + LicenseConstant.LICENSE_FILE_EXTENSION;
log.info("新证书已重命名为:{}", newLicenseFileName);
log.info("++++++++ 默认证书更换结束 ++++++++");

测试过程有问题,不应该在启动项目时测试,启动项目时安装默认安装默认证书,根本测试不出来证书是否更新,要留意文件更新时间。

先搞定现场服务器硬件信息校验,抽取代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* 获取现场服务器硬件信息
*
* @return LicenseCheckModel
*/
public LicenseCheckModel getHardwareInfoFromEnv() {
// 获取容器环境变量
LicenseCheckModel hardwareInfo = new LicenseCheckModel();
String ipAddressEnv = System.getenv(LicenseConstant.IP_ADDRESS_ENV); // 获取容器 IP 地址
String MacAddressEnv = System.getenv(LicenseConstant.MAC_ADDRESS_ENV); // 获取容器 Mac 地址
String CpuSerialEnv = System.getenv(LicenseConstant.CPU_SERIAL_ENV); // 获取容器 CPU 序列号
String MainBoardSerialEnv = System.getenv(LicenseConstant.MAINBOARD_SERIAL_ENV); // 获取容器主板序列号
// 封装现场服务器硬件信息
if (ObjectUtils.isNotEmpty(ipAddressEnv)) {
hardwareInfo.setIpAddress(Lists.newArrayList(ipAddressEnv.split(",")));
}
if (ObjectUtils.isNotEmpty(MacAddressEnv)) {
hardwareInfo.setMacAddress(Lists.newArrayList(MacAddressEnv.split(",")));
}
if (ObjectUtils.isNotEmpty(CpuSerialEnv)) {
hardwareInfo.setCpuSerial(CpuSerialEnv);
}
if (ObjectUtils.isNotEmpty(MainBoardSerialEnv)) {
hardwareInfo.setMainBoardSerial(MainBoardSerialEnv);
}
if (ObjectUtils.isEmpty(hardwareInfo) || ObjectUtils.isEmpty(hardwareInfo.getIpAddress()) || ObjectUtils.isEmpty(hardwareInfo.getMacAddress()) || ObjectUtils.isEmpty(hardwareInfo.getCpuSerial()) || ObjectUtils.isEmpty(hardwareInfo.getMainBoardSerial())) {
log.error("获取现场服务器硬件信息失败!");
throw new CommonException(401, "获取现场服务器硬件信息失败!");
}
// 返回现场服务器硬件信息
return hardwareInfo;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 安装证书
try {
LicenseParam licenseParam = initLicenseParam(param);
LicenseManager licenseManager = LicenseManagerHolder.getInstance(licenseParam);
// 删除旧证书
licenseManager.uninstall();
// 获取现场服务器硬件信息
LicenseCheckModel hardwareInfo = this.getHardwareInfoFromEnv();
// 设置现场服务器硬件信息
HardwareInfoContext.setHardwareInfo(hardwareInfo);
// result = licenseManager.install(new File(param.getLicensePath()));
result = new CustomLicenseManager(licenseParam).install(new File(param.getLicensePath()));
log.info("证书安装成功");
log.info(MessageFormat.format("证书安装成功,证书有效期:{0} - {1}", format.format(result.getNotBefore()), format.format(result.getNotAfter())));
log.info("证书内容:{}", result);
// 更换默认证书, 保证项目重启后安装最新证书
changeDefaultLicense(param);
} catch (Exception e) {
log.error("证书安装失败!", e);
throw new CommonException(401, "证书安装失败");
// System.exit(0);
}

好像成功了,稀里糊涂的,再测试一下。

1
2
3
4
5
6
7
8
9
// 执行校验
try {
result = licenseManager.verify();
log.info(MessageFormat.format("证书校验通过,证书有效期:{0} - {1}", format.format(result.getNotBefore()), format.format(result.getNotAfter())));
} catch (Exception e) {
log.error("证书校验失败!", e);
System.exit(0);
throw new CommonException(401, "证书校验失败");
}

想优化下证书校验逻辑的,证书校验通过后返回的信息比较有限,没有携带证书附加信息比如证书描述这些。

后续再看看吧。

哦,原来是有的,licenseContentVO,封装了硬件信息。

服务端,获取证书内容;客户端,证书安装,证书校验,这些处理结果的信息需要优化,是否显示硬件信息,以及格式。

1
更换默认证书, 保证项目重启后安装最新证书。
1
2
3
4
5
6
// 更换默认证书, 保证项目重启后安装最新证书
Boolean changed = changeDefaultLicense(param);
if (!changed) {
log.error("默认证书更换失败");
throw new CommonException(401, "默认证书更换失败");
}

日志打印,字符串拼接。

证书安装完成后,输出证书有效期。

1
log.info(MessageFormat.format("证书安装成功,证书有效期:{1} - {2}", format.format(result.getNotBefore()), format.format(result.getNotAfter())));

同样的,证书校验完成也是如此。

1
log.info(MessageFormat.format("证书校验成功,证书有效期:{1} - {2}", format.format(result.getNotBefore()), format.format(result.getNotAfter())));

证书安装通过后返回的信息比较有限,没有携带证书附加信息,比如证书描述。

1
log.info("证书描述:" + result.getInfo() + MessageFormat.format("证书安装成功,证书有效期:{0} - {1}", format.format(result.getNotBefore()), format.format(result.getNotAfter())));

使用String.format统一下。

1
2
3
4
log.info(String.format("证书安装成功,证书描述:%s,证书有效期:%s - %s",
result.getInfo(),
format.format(result.getNotBefore()),
format.format(result.getNotAfter())));

删除 Git 存储库中的分支 - Azure Repos | Microsoft Learn

搞了搞分支,昨天下午推送的代码有问题,不全。

客户端代码基本完善,不过现在本地测试已经全面失败了,需要获取到本机环境变量以填充现场服务器硬件信息。

docker 部署。

要不先学学昨天接触到的 docker 远程部署。

1
Only key-pair ssh auth type is supported for docker connections.

远程部署成功!

image-20250207155252693

2025 年 2 月 8 日

今天整改所有的硬件信息校验,只需携带 IP 地址和 MAC 地址即可,CPU 序列号和主板序列号的校验不需要了。

1
整改硬件信息校验流程,许可证无需携带CPU序列号和主板序列号
1
java.sql.SQLException: Unknown column 'cpu_serial' in 'order clause' at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:120) at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122)
1
整改硬件信息校验流程,许可证无需携带CPU序列号和主板序列号。

推送过程中出现了点小问题,IDEA 竟然不管理 backend-server 模块下的代码了。

怪不得前天下午提交代码后,没有推送最新后台资源管理系统的最新代码,原来更换了远程仓库以后就出现问题了。

得解决一下。

(24 封私信 / 69 条消息) Java中trim()方法是什么?有什么用? - 知乎 (zhihu.com)

校验注解:@Valid 和 @Validated区别与用法(附详细案例)_valid validated-CSDN博客

安装失败,捕获异常,抛出异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 安装证书
try {
LicenseParam licenseParam = initLicenseParam(param);
LicenseManager licenseManager = LicenseManagerHolder.getInstance(licenseParam);
// 删除旧证书
licenseManager.uninstall();
// 获取现场服务器硬件信息
LicenseCheckModel hardwareInfo = this.getHardwareInfoFromEnv();
// 设置现场服务器硬件信息
HardwareInfoContext.setHardwareInfo(hardwareInfo);
// 证书安装(校验现场服务器硬件信息)
result = new CustomLicenseManager(licenseParam).install(new File(param.getLicensePath()));
log.info("证书安装成功");
log.info(String.format("证书安装成功,证书描述:%s,证书有效期:%s - %s", result.getInfo(), format.format(result.getNotBefore()), format.format(result.getNotAfter())));
log.info("证书内容:{}", result);
// 更换默认证书, 保证项目重启后安装最新证书
Boolean changed = changeDefaultLicense(param);
if (!changed) {
log.error("默认证书更换失败");
throw new CommonException(401, "默认证书更换失败");
}
} catch (Exception e) {
log.error("证书安装失败", e);
throw new CommonException(401, "证书安装失败");
// System.exit(0);
}

去掉 try-catch 块返回的提示信息可能会更加直观一些,不然可能会吞掉异常。

傻了,用不着那么干,这样就行:

1
2
3
4
5
} catch (Exception e) {
log.error("证书安装失败", e);
throw new CommonException(401, e.getMessage());
// System.exit(0);
}

现在的问题是,为什么安装新证书失败后,项目直接死掉了。

1
2
3
dahuatech-icc- 2025-02-08 13:52:30 [scheduling-1] INFO  com.th.cloud.iois.common.license.license.LicenseVerify:163 - ++++++++ 开始验证证书 ++++++++
dahuatech-icc- 2025-02-08 13:52:30 [scheduling-1] ERROR com.th.cloud.iois.common.license.license.LicenseVerify:172 - 证书校验失败
de.schlichtherle.license.NoLicenseInstalledException: zuiyu_demo

看见了,校验证书失败,死掉了。

不对。

新证书都没安装成功,旧证书当然是可以正常使用的,怎么会校验失败。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 删除旧证书
licenseManager.uninstall();
// 获取现场服务器硬件信息
LicenseCheckModel hardwareInfo = this.getHardwareInfoFromEnv();
// 设置现场服务器硬件信息
HardwareInfoContext.setHardwareInfo(hardwareInfo);
// 证书安装(校验现场服务器硬件信息)
result = new CustomLicenseManager(licenseParam).install(new File(param.getLicensePath()));
log.info("证书安装成功");
log.info(String.format("证书安装成功,证书描述:%s,证书有效期:%s - %s", result.getInfo(), format.format(result.getNotBefore()), format.format(result.getNotAfter())));
log.info("证书内容:{}", result);
// 更换默认证书, 保证项目重启后安装最新证书
Boolean changed = changeDefaultLicense(param);
if (!changed) {
log.error("默认证书更换失败");
throw new CommonException(401, "默认证书更换失败");
}

看明白了,尽管未安装新证书,但旧证书还是删除掉了。艹。

1
2
3
4
// 删除旧证书
licenseManager.uninstall();
// 证书安装(校验现场服务器硬件信息)
result = new CustomLicenseManager(licenseParam).install(new File(param.getLicensePath()));

这样,删除旧证书一定要放在证书安装之前,两个步骤一定是紧紧耦合的。

呸。

那要是证书安装过程中,出现了诸如硬件信息校验不通过的状况,那岂不是同样的问题又出现了。

删除旧证书放在证书成功安装之后?那不就是把新安装的证书删了。

只有两种可靠的方法:试试不删除旧证书能否安装新证书,如果可以就这么办;或者在真正执行安装证书之前才删除旧证书。

测试,不删除证书,注释掉获取现场服务器硬件信息,校验现场服务器硬件信息,试试看证书能不能安装成功。

。。见鬼了。

新证书确实安装成功了,旧证书也删除了,默认证书也替换了,可实时定时任务执行后返回的证书内容竟然还是旧证书,之前没出过问题。

重启下项目就正常了,新证书确实安装成功了,校验证书也通过的,信息没有错误。

不删除旧证书会对校验证书有影响。

1

新增证书安装参数上下文。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 证书安装参数上下文
*/
public class LicenseParamContext {
private static final ThreadLocal<LicenseParam> threadLocal = new ThreadLocal<>();

public static void setLicenseParam(LicenseParam hardwareInfo) {
threadLocal.set(hardwareInfo);
}

public static LicenseParam getLicenseParam() {
return threadLocal.get();
}

public static void clear() {
threadLocal.remove();
}
}

证书安装前,用 threadLocal 保存安装参数。

1
2
3
4
// 证书安装(校验现场服务器硬件信息)
// 设置证书安装时的参数
LicenseParamContext.setLicenseParam(licenseParam);
result = new CustomLicenseManager(licenseParam).install(new File(param.getLicensePath()));

硬件信息校验完毕后,真正执行证书安装之前,执行删除旧证书。

1
2
3
4
5
6
7
8
9
// 校验证书信息
// 复写validate方法,增加IP地址、Mac地址等其他信息校验
this.validate(content);
// 删除旧证书
LicenseManager licenseManager = LicenseManagerHolder.getInstance(LicenseParamContext.getLicenseParam());
licenseManager.uninstall();
// 保存证书信息
setLicenseKey(key);
setCertificate(certificate);

完毕,测试安装新证书。

问题解决,特么多么干净清爽正确的日志,符合预期。

1
2
3
4
5
6
7
8
9
10
11
12
dahuatech-icc- 2025-02-08 14:36:37 [http-nio-5012-exec-2] INFO  com.th.cloud.iois.common.license.license.LicenseVerify:63 - ++++++++ 开始安装证书 ++++++++
dahuatech-icc- 2025-02-08 14:36:37 [http-nio-5012-exec-2] INFO com.th.cloud.iois.common.license.license.LicenseVerify:77 - 证书安装成功
dahuatech-icc- 2025-02-08 14:36:37 [http-nio-5012-exec-2] INFO com.th.cloud.iois.common.license.license.LicenseVerify:78 - 证书安装成功,证书描述:新证书2,证书有效期:2025-02-05 16:01:21 - 2025-05-21 16:01:21
dahuatech-icc- 2025-02-08 14:36:37 [http-nio-5012-exec-2] INFO com.th.cloud.iois.common.license.license.LicenseVerify:79 - 证书内容:de.schlichtherle.license.LicenseContent@6899d9b5
dahuatech-icc- 2025-02-08 14:36:37 [http-nio-5012-exec-2] INFO com.th.cloud.iois.common.license.license.LicenseVerify:129 - ++++++++ 开始更换默认证书 ++++++++
dahuatech-icc- 2025-02-08 14:36:37 [http-nio-5012-exec-2] INFO com.th.cloud.iois.common.license.license.LicenseVerify:135 - 已删除默认证书以准备重命名新证书:D:\Project\iois-whl-svr\th-iois-file-server\license\default.lic
dahuatech-icc- 2025-02-08 14:36:37 [http-nio-5012-exec-2] INFO com.th.cloud.iois.common.license.license.LicenseVerify:151 - 新证书已重命名为:default.lic
dahuatech-icc- 2025-02-08 14:36:37 [http-nio-5012-exec-2] INFO com.th.cloud.iois.common.license.license.LicenseVerify:152 - ++++++++ 默认证书更换结束 ++++++++
dahuatech-icc- 2025-02-08 14:36:37 [http-nio-5012-exec-2] INFO com.th.cloud.iois.common.license.license.LicenseVerify:91 - ++++++++ 证书安装结束 ++++++++
dahuatech-icc- 2025-02-08 14:36:40 [scheduling-1] INFO com.th.cloud.iois.common.license.license.LicenseVerify:162 - ++++++++ 开始验证证书 ++++++++
dahuatech-icc- 2025-02-08 14:36:40 [scheduling-1] INFO com.th.cloud.iois.common.license.license.LicenseVerify:169 - 证书校验成功,证书描述:新证书2,证书有效期:2025-02-05 16:01:21 - 2025-05-21 16:01:21
dahuatech-icc- 2025-02-08 14:36:40 [scheduling-1] INFO com.th.cloud.iois.common.license.license.LicenseVerify:175 - ++++++++ 证书验证结束 ++++++++

同样的,证书安装失败后也不会影响到原有证书。

1
2
3
4
5
6
7
com.th.cloud.iois.common.exception.CommonException: 证书序号不符合规范
at com.th.cloud.iois.file.controller.LicenseController.install(LicenseController.java:91)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.lang.Thread.run(Thread.java:748)
dahuatech-icc- 2025-02-08 14:43:50 [scheduling-1] INFO com.th.cloud.iois.common.license.license.LicenseVerify:162 - ++++++++ 开始验证证书 ++++++++
dahuatech-icc- 2025-02-08 14:43:50 [scheduling-1] INFO com.th.cloud.iois.common.license.license.LicenseVerify:169 - 证书校验成功,证书描述:新证书2,证书有效期:2025-02-05 16:01:21 - 2025-05-21 16:01:21
dahuatech-icc- 2025-02-08 14:43:50 [scheduling-1] INFO com.th.cloud.iois.common.license.license.LicenseVerify:175 - ++++++++ 证书验证结束 ++++++++
1
2
3
4
5
6
7
com.th.cloud.iois.common.exception.CommonException: 证书序号不能为空
at com.th.cloud.iois.file.controller.LicenseController.install(LicenseController.java:87)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
dahuatech-icc- 2025-02-08 14:44:20 [scheduling-1] INFO com.th.cloud.iois.common.license.license.LicenseVerify:162 - ++++++++ 开始验证证书 ++++++++
dahuatech-icc- 2025-02-08 14:44:20 [scheduling-1] INFO com.th.cloud.iois.common.license.license.LicenseVerify:169 - 证书校验成功,证书描述:新证书2,证书有效期:2025-02-05 16:01:21 - 2025-05-21 16:01:21
dahuatech-icc- 2025-02-08 14:44:20 [scheduling-1] INFO com.th.cloud.iois.common.license.license.LicenseVerify:175 - ++++++++ 证书验证结束 ++++++++
1
2
3
4
5
6
7
com.th.cloud.iois.common.exception.CommonException: 获取现场服务器硬件信息失败
at com.th.cloud.iois.common.license.license.LicenseVerify.install(LicenseVerify.java:88)
at com.th.cloud.iois.common.license.service.impl.LicenseServiceImpl.install(LicenseServiceImpl.java:231)
at com.th.cloud.iois.file.controller.LicenseController.install(LicenseController.java:94)
dahuatech-icc- 2025-02-08 14:51:00 [scheduling-1] INFO com.th.cloud.iois.common.license.license.LicenseVerify:162 - ++++++++ 开始验证证书 ++++++++
dahuatech-icc- 2025-02-08 14:51:00 [scheduling-1] INFO com.th.cloud.iois.common.license.license.LicenseVerify:169 - 证书校验成功,证书描述:测试证书3,证书有效期:2025-02-05 16:00:59 - 2025-04-12 16:00:59
dahuatech-icc- 2025-02-08 14:51:00 [scheduling-1] INFO com.th.cloud.iois.common.license.license.LicenseVerify:175 - ++++++++ 证书验证结束 ++++++++
1
删除已安装证书滞后,避免安装新证书失败后导致原持有证书校验失败。

眼前寇需解决的问题,是管理证书生成,安装和校验环节中的返回信息规范性问题,输出内容要统一。

特么的今天删除俩传参后两次出现这个报错,原来是识别参数失败,“}”号后面多携带了一个“,”号。

1
2
org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Unexpected character ('}' (code 125)): was expecting double-quote to start field name; nested exception is com.fasterxml.jackson.databind.JsonMappingException: Unexpected character ('}' (code 125)): was expecting double-quote to start field name
at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 6, column: 3] (through reference chain: com.common.server.entity.license.create.LicenseCreatorParamRequest["licenseCheckModel"])
1
2
3
4
5
6
7
8
9
10
{
"applicantName": "黄天柱",
"licenseCheckModel": {
"ipAddress": ["172.17.0.1","172.19.139.126"],
"macAddress": ["02-42-8C-B0-ED-AC","00-16-3E-34-9B-2E"],
},
"licenseName": "测试新证书",
"projectId": 1,
"validityDays": 66
}

这是服务端证书生成后的返回结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
{
"code": 0,
"data": {
"holder": {
"name": "CN=localhost,OU=localhost,O=localhost,L=SH,ST=SH,C=CN",
"encoded": "MGMxCzAJBgNVBAYTAkNOMQswCQYDVQQIEwJTSDELMAkGA1UEBxMCU0gxEjAQBgNVBAoTCWxvY2FsaG9zdDESMBAGA1UECxMJbG9jYWxob3N0MRIwEAYDVQQDEwlsb2NhbGhvc3Q="
},
"issuer": {
"name": "CN=localhost,OU=localhost,O=localhost,L=SH,ST=SH,C=CN",
"encoded": "MGMxCzAJBgNVBAYTAkNOMQswCQYDVQQIEwJTSDELMAkGA1UEBxMCU0gxEjAQBgNVBAoTCWxvY2FsaG9zdDESMBAGA1UECxMJbG9jYWxob3N0MRIwEAYDVQQDEwlsb2NhbGhvc3Q="
},
"subject": "zuiyu_demo",
"issued": "2025-02-08T07:04:53.953+00:00",
"notBefore": "2025-02-08T07:04:53.953+00:00",
"notAfter": "2025-04-15T07:04:53.953+00:00",
"consumerType": "User",
"consumerAmount": 1,
"info": "测试新证书",
"extra": {
"ipAddress": [
"172.17.0.1",
"172.19.139.126"
],
"macAddress": [
"02-42-8C-B0-ED-AC",
"00-16-3E-34-9B-2E"
]
}
},
"message": "License 证书生成成功"
}

这是服务端证书内容解析后的返回结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
{
"code": 0,
"data": {
"holder": {
"name": "CN=localhost,OU=localhost,O=localhost,L=SH,ST=SH,C=CN",
"encoded": "MGMxCzAJBgNVBAYTAkNOMQswCQYDVQQIEwJTSDELMAkGA1UEBxMCU0gxEjAQBgNVBAoTCWxvY2FsaG9zdDESMBAGA1UECxMJbG9jYWxob3N0MRIwEAYDVQQDEwlsb2NhbGhvc3Q="
},
"issuer": {
"name": "CN=localhost,OU=localhost,O=localhost,L=SH,ST=SH,C=CN",
"encoded": "MGMxCzAJBgNVBAYTAkNOMQswCQYDVQQIEwJTSDELMAkGA1UEBxMCU0gxEjAQBgNVBAoTCWxvY2FsaG9zdDESMBAGA1UECxMJbG9jYWxob3N0MRIwEAYDVQQDEwlsb2NhbGhvc3Q="
},
"subject": "zuiyu_demo",
"issued": "2025-02-05T08:24:42.274+00:00",
"notBefore": "2025-02-05T08:24:42.274+00:00",
"notAfter": "2025-04-12T08:24:42.274+00:00",
"consumerType": "User",
"consumerAmount": 1,
"info": "正月初八新证书",
"extra": {
"ipAddress": [
"172.17.0.1",
"172.19.139.126"
],
"macAddress": [
"02-42-8C-B0-ED-AC",
"00-16-3E-34-9B-2E"
]
}
},
"message": "获取证书内容成功"
}

那客户端证书安装成功以及校验成功以后的返回结果是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"code": 0,
"msg": "证书校验成功",
"data": {
"holder": {
"name": "CN=localhost,OU=localhost,O=localhost,L=SH,ST=SH,C=CN",
"encoded": "MGMxCzAJBgNVBAYTAkNOMQswCQYDVQQIEwJTSDELMAkGA1UEBxMCU0gxEjAQBgNVBAoTCWxvY2FsaG9zdDESMBAGA1UECxMJbG9jYWxob3N0MRIwEAYDVQQDEwlsb2NhbGhvc3Q="
},
"issuer": {
"name": "CN=localhost,OU=localhost,O=localhost,L=SH,ST=SH,C=CN",
"encoded": "MGMxCzAJBgNVBAYTAkNOMQswCQYDVQQIEwJTSDELMAkGA1UEBxMCU0gxEjAQBgNVBAoTCWxvY2FsaG9zdDESMBAGA1UECxMJbG9jYWxob3N0MRIwEAYDVQQDEwlsb2NhbGhvc3Q="
},
"subject": "zuiyu_demo",
"issued": "2025-02-05T08:00:59.245+00:00",
"notBefore": "2025-02-05T08:00:59.245+00:00",
"notAfter": "2025-04-12T08:00:59.245+00:00",
"consumerType": "User",
"consumerAmount": 1,
"info": "测试证书3",
"extra": "{\"ipAddress\":[\"172.17.0.1\",\"172.19.139.126\"],\"macAddress\":[\"02-42-8C-B0-ED-AC\",\"00-16-3E-34-9B-2E\"],\"cpuSerial\":\"54 06 05 00 FF FB 8B 1F\",\"mainBoardSerial\":\"a5e5ccee-7c25-4864-9c59-26fd2886f7e4\"}"
},
"success": true
}

证书携带的额外信息(服务器硬件信息)竟然是杂乱的 json 字符串。

更新一下。

1
2
3
4
5
6
log.info("++++++++ 证书安装结束 ++++++++");
Gson gson = new Gson();
LicenseContentVO licenseContentVO = new LicenseContentVO();
BeanUtils.copyProperties(result, licenseContentVO);
licenseContentVO.setExtra(gson.fromJson(result.getExtra().toString(), LicenseCheckModel.class));
return licenseContentVO;
1
2
3
4
5
6
log.info("++++++++ 证书验证结束 ++++++++");
Gson gson = new Gson();
LicenseContentVO licenseContentVO = new LicenseContentVO();
BeanUtils.copyProperties(result, licenseContentVO);
licenseContentVO.setExtra(gson.fromJson(result.getExtra().toString(), LicenseCheckModel.class));
return licenseContentVO;

很好,已统一。

封装一下,都是冗余代码。

1
2
// 封装并返回证书内容
return getLicenseContentVO(result);
1
2
3
4
5
6
7
8
9
10
11
12
/**
* 封装证书内容
*
* @param result LicenseContent 证书内容
*/
private LicenseContentVO getLicenseContentVO(LicenseContent result) {
Gson gson = new Gson();
LicenseContentVO licenseContentVO = new LicenseContentVO();
BeanUtils.copyProperties(result, licenseContentVO);
licenseContentVO.setExtra(gson.fromJson(result.getExtra().toString(), LicenseCheckModel.class));
return licenseContentVO;
}
1
封装证书生成,安装和校验所获取证书内容,统一返回结果。

目前没有发现新的问题,最后两个任务:

测试环境使用 docker 部署测试运行实际情况,排查线上问题;更新密钥对。

下周再说更新密钥对的事情,今天搞搞 docker 项目部署,远程部署连接测试服务器失败,本地 Maven 打包还有问题。

试试看。

IDEA+Docker远程部署Sprin - 编程导航 - 程序员编程学习交流社区 (codefather.cn)

在IDEA中通过密钥认证的方式使用SSH连接远程Linux服务器_idea ssh密码登录-CSDN博客

SSH。

1
2
Can not construct instance of com.spotify.docker.client.messages.RegistryAuth: no String-argument constructor/factory method to deserialize from String value ('desktop')
at [Source: N/A; line: -1, column: -1] (through reference chain: java.util.LinkedHashMap["credsStore"])

人生如戏。

倒在同样的问题前,前天,昨天都是打包的问题,原以为远程部署可以绕开打包这一环节,但直到真正开启部署的时候我才明白:

都是一回事。

下周见,我的朋友,今天辛苦了。

2025 年 2 月 10 日

这么快又见面了,问题还没解决呢。

本机虚拟机安装及网络配置完全结束,阿里云服务器可以停用了。

安装 Docker。

wget、yum、rpm、apt-get区别「建议收藏」-腾讯云开发者社区-腾讯云 (tencent.com)

建库建表

2024 年 12 月 25 日

连接数据库。

1
2
3
4
5
6
<!--MySQL-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
1
2
3
4
5
# MySQL
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://mysql.tellhow.com:3306/WHL_IOIS_STATION?zeroDateTimeBehavior=convertToNull
spring.datasource.username=root
spring.datasource.password=root

image-20241225102038953

image-20241225102354762

借助 SQL 之父平台快速建库建表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
-- 许可证信息
create database if not exists iois_backend;
use iois_backend;

create table if not exists iois_backend.`license_info`
(
`id` bigint not null auto_increment comment '序号' primary key,
`license_name` varchar(1024) not null comment '证书名',
`project_id` varchar(1024) not null comment '所属项目',
`IP` varchar(256) not null comment 'IP',
`MAC` varchar(256) not null comment 'MAC',
`issued_time` datetime not null comment '生效时间',
`expiry_time` datetime not null comment '失效时间',
`validity_days` varchar(256) not null comment '许可期限',
`applicant_name` varchar(256) not null comment '申请人',
`status` int(1) default '0' not null comment '证书状态',
`is_deleted` int(1) default '0' not null comment '是否删除',
`create_time` datetime not null on update CURRENT_TIMESTAMP comment '创建时间',
`update_time` datetime not null on update CURRENT_TIMESTAMP comment '修改时间'
) comment '许可证信息';
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- 项目信息
create table if not exists iois_backend.`project_info`
(
`id` bigint not null auto_increment comment '序号' primary key,
`project_name` varchar(1024) not null comment '证书名',
`project_number` varchar(256) not null comment '项目编号',
`product_name` varchar(256) not null comment '所属产品',
`project_code` varchar(256) not null comment '项目代码',
`manager` varchar(256) not null comment '项目经理',
`项目描述` varchar(1024) not null comment '项目描述',
`status` int(1) default '0' not null comment '项目状态',
`is_deleted` int(1) default '0' not null comment '是否删除',
`create_time` datetime not null on update CURRENT_TIMESTAMP comment '创建时间',
`update_time` datetime not null on update CURRENT_TIMESTAMP comment '修改时间'
) comment '项目信息';

image-20241225173953152

建库建表完成,导入 Mybatis 实现数据库增删改查:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus-spring-boot-starter.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-extension</artifactId>
<version>${mybatis-plus-extension.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.4.0</version>
</dependency>

image-20241225135503358

1
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'projectInfoServiceImpl': Unsatisfied dependency expressed through field 'baseMapper'; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.memory.project_server.mapper.ProjectInfoMapper' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
1
2
3
4
@Mapper
public interface ProjectInfoMapper extends BaseMapper<ProjectInfo> {

}
1
2
3
4
5
6
7
@SpringBootApplication
@MapperScan("com.project.server.mapper")
public class ProjectServerApplication {
public static void main(String[] args) {
SpringApplication.run(ProjectServerApplication.class, args);
}
}

妈的测试查询又报错,原来是这个项目启动类没添加 @MapperScan 注解:

image-20241225141520148

1
rg.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'licenseInfoServiceImpl': Unsatisfied dependency expressed through field 'baseMapper'; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.license.server.mapper.LicenseInfoMapper' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}

跑通了,接下来实现 License 证书保存。

基础巩固

2024 年 12 月 25 日

【SpringBoot】初学SpringMVC必备知识详解-CSDN博客

2024 年 12 月 27 日

Linux基础-vim命令详解(理论+实战)_linux vim-CSDN博客

2024 年 12 月 31 日

过滤器,拦截器,QueryMapper 查询匹配器,IO流。

2025 年 1 月 2 日

1
2
3
4
5
6
7
8
9
10
/**
* 许可证信息
*/
@EqualsAndHashCode(callSuper = true)
@Data
@ApiModel(value = "许可证信息")
public class LicenseInfoVO extends BaseEntity implements Serializable {

........................
}

@EqualsAndHashCode 是 Lombok 库提供的一个注解,用于自动生成 Java 类的 equals()hashCode() 方法。这两个方法在 Java 中非常重要,尤其是在将对象用作哈希表(如 HashMapHashSet 等)的键时,或者当你需要比较对象是否相等时。

callSuper=true 参数是 @EqualsAndHashCode 注解的一个选项,它指示 Lombok 在生成 equals()hashCode() 方法时应该包含对父类 equals()hashCode() 方法的调用。这通常在你希望子类的相等性判断也考虑父类字段时是有用的。

2025 年 1 月 3 日

(一)漫谈分布式开篇:从全景视野详解单体到分布式架构的蜕变之旅!很多人做过分布式项目,但却不具备分布式经验,为啥?对大多 - 掘金 (juejin.cn)

打包

2024 年 12 月 27 日

spring-boot-maven-plugin插件-CSDN博客

springboot(二)———解决Maven打包失败:程序包XXX不存在_maven 打包报错,程序包不存在-CSDN博客

部署

我特么竟然一步到位部署成功了,真就当我什么都会是吧,还好我什么都会一点。

image-20241226115521653

打 jar 包:

image-20241226122044993

找到 jar 包,本地 Linux 虚拟机远程连接服务器:

1
2
3
192.168.118.16
root
rootmax

image-20241226122202276

在 /home 目录下创建自己的目录:

1
/home/memory/iois/backend/license/

在这个目录下扔进来 jar 包,这边有一个脚本文件(restart_license_server.sh),一键启动运行,相当方便:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#!/bin/bash
#jar包文件路径及名称(目录按照各自配置)
APP_NAME=/home/memory/iois/backend/license/license-server-0.0.1-SNAPSHOT.jar
#日志文件路径及名称(目录按照各自配置)
LOG_FILE=/home/memory/iois/backend/license/license-server.log
CONFIG_FILE_PATH=/home/memory/iois/backend/license/application.properties

#查询进程,并杀掉当前jar/java程序

pid=`ps -ef|grep $APP_NAME | grep -v grep | awk '{print $2}'`
kill -9 $pid
echo "$pid进程终止成功"

sleep 2

#判断jar包文件是否存在,如果存在启动jar包,并时时查看启动日志

if test -e $APP_NAME
then
echo '文件存在,开始启动此程序...'

# 启动jar包,指向日志文件,2>&1 & 表示打开或指向同一个日志文件
nohup java -jar $APP_NAME --spring.config.location=$CONFIG_FILE_PATH > $LOG_FILE 2>&1 &

#实时查看启动日志(此处正在想办法启动成功后退出)
tail -f $LOG_FILE

#输出启动成功(上面的查看日志没有退出,所以执行不了,可以去掉)

#echo '$APP_NAME 启动成功...'
else
echo '$APP_NAME 文件不存在,请检查。'
fi

分别把 jar 包,日志文件,配置文件放在指定目录下,这个脚本中的配置相应也要修改成指定目录。

image-20241226122543793

打开页面拖进来就行了,拖到指定目录下。

执行脚本之前,先赋予可执行权限,要不然会报错:

1
chmod +x ./restart_license_server.sh 

执行脚本:

1
./restart_license_server.sh

如你所见,启动成功:

image-20241226122938889

访问下 Swagger 接口文档地址:http://192.168.118.16:8081/license-server/doc.html

1
http://192.168.118.16:8081/license-server/doc.html

image-20241226131008912

用户管理模块开发完毕后就可以着手部署启动项目了。

启动失败:

image-20241226173903596

在Linux中杀死占用某个端口的进程_linux 杀死端口号进程-CSDN博客

1
netstat -tunlp | grep 8081
1
lsof -i :8081

image-20241226173820565

起来了,起来了。

image-20241226174202802

成功访问后台管理接口文档:后台管理接口文档

image-20241226174922168

1
2
3
4
5
Caused by: java.net.ConnectException: 拒绝连接 (Connection refused)
at java.net.PlainSocketImpl.socketConnect(Native Method) ~[na:1.8.0_412]
at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350) ~[na:1.8.0_412]
at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206) ~[na:1.8.0_412]
at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188) ~[na:1.8.0_412]

Caused by: java.net.ConnectException: Connection refused (Connection refused) at java.net.PlainSock-CSDN博客

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.PersistenceException: 
### Error querying database. Cause: org.springframework.jdbc.CannotGetJdbcConnectionException: Failed to obtain JDBC Connection; nested exception is com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure

The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server.
### The error may exist in com/common/server/mapper/UserInfoMapper.java (best guess)
### The error may involve com.common.server.mapper.UserInfoMapper.selectCount
### The error occurred while executing a query
### Cause: org.springframework.jdbc.CannotGetJdbcConnectionException: Failed to obtain JDBC Connection; nested exception is com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure

The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server.
at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:92) ~[mybatis-spring-2.0.5.jar!/:2.0.5]
at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:440) ~[mybatis-spring-2.0.5.jar!/:2.0.5]
at com.sun.proxy.$Proxy81.selectOne(Unknown Source) ~[na:na]
at org.mybatis.spring.SqlSessionTemplate.selectOne(SqlSessionTemplate.java:159) ~[mybatis-spring-2.0.5.jar!/:2.0.5]
at com.baomidou.mybatisplus.core.override.MybatisMapperMethod.execute(MybatisMapperMethod.java:90) ~[mybatis-plus-core-3.4.2.jar!/:3.4.2]
at com.baomidou.mybatisplus.core.override.MybatisMapperProxy$PlainMethodInvoker.invoke(MybatisMapperProxy.java:148) ~[mybatis-plus-core-3.4.2.jar!/:3.4.2]
at com.baomidou.mybatisplus.core.override.MybatisMapperProxy.invoke(MybatisMapperProxy.java:89) ~[mybatis-plus-core-3.4.2.jar!/:3.4.2]
at com.sun.proxy.$Proxy83.selectCount(Unknown Source) ~[na:na]
at com.backend.server.service.impl.UserInfoServiceImpl.userRegister(UserInfoServiceImpl.java:61) ~[classes!/:0.0.1-SNAPSHOT]
at com.backend.server.service.impl.UserInfoServiceImpl$$FastClassBySpringCGLIB$$836df4b.invoke(<generated>) ~[classes!/:0.0.1-SNAPSHOT]
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) ~[spring-core-5.3.24.jar!/:5.3.24]
at org.springframework.aop.framework.CglibAopProxy.invokeMethod(CglibAopProxy.java:386) ~[spring-aop-5.3.24.jar!/:5.3.24]
at org.springframework.aop.framework.CglibAopProxy.access$000(CglibAopProxy.java:85) ~[spring-aop-5.3.24.jar!/:5.3.24]
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:704) ~[spring-aop-5.3.24.jar!/:5.3.24]
at com.backend.server.service.impl.UserInfoServiceImpl$$EnhancerBySpringCGLIB$$832a1c9.userRegister(<generated>) ~[classes!/:0.0.1-SNAPSHOT]
at com.backend.server.controller.UserInfoController.userRegister(UserInfoController.java:67) ~[classes!/:0.0.1-SNAPSHOT]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_412]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_412]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_412]
at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_412]
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205) ~[spring-web-5.3.24.jar!/:5.3.24]
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:150) ~[spring-web-5.3.24.jar!/:5.3.24]
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117) ~[spring-webmvc-5.3.24.jar!/:5.3.24]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895) ~[spring-webmvc-5.3.24.jar!/:5.3.24]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808) ~[spring-webmvc-5.3.24.jar!/:5.3.24]
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-5.3.24.jar!/:5.3.24]
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1071) ~[spring-webmvc-5.3.24.jar!/:5.3.24]
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:964) ~[spring-webmvc-5.3.24.jar!/:5.3.24]
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) [spring-webmvc-5.3.24.jar!/:5.3.24]
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909) [spring-webmvc-5.3.24.jar!/:5.3.24]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:696) [tomcat-embed-core-9.0.70.jar!/:na]
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) [spring-webmvc-5.3.24.jar!/:5.3.24]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:779) [tomcat-embed-core-9.0.70.jar!/:na]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227) [tomcat-embed-core-9.0.70.jar!/:na]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) [tomcat-embed-core-9.0.70.jar!/:na]
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) [tomcat-embed-websocket-9.0.70.jar!/:na]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) [tomcat-embed-core-9.0.70.jar!/:na]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) [tomcat-embed-core-9.0.70.jar!/:na]
at com.github.xiaoymin.swaggerbootstrapui.filter.SecurityBasicAuthFilter.doFilter(SecurityBasicAuthFilter.java:84) [swagger-bootstrap-ui-1.9.6.jar!/:na]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) [tomcat-embed-core-9.0.70.jar!/:na]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) [tomcat-embed-core-9.0.70.jar!/:na]
at com.github.xiaoymin.swaggerbootstrapui.filter.ProductionSecurityFilter.doFilter(ProductionSecurityFilter.java:53) [swagger-bootstrap-ui-1.9.6.jar!/:na]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) [tomcat-embed-core-9.0.70.jar!/:na]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) [tomcat-embed-core-9.0.70.jar!/:na]
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) [spring-web-5.3.24.jar!/:5.3.24]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) [spring-web-5.3.24.jar!/:5.3.24]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) [tomcat-embed-core-9.0.70.jar!/:na]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) [tomcat-embed-core-9.0.70.jar!/:na]
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) [spring-web-5.3.24.jar!/:5.3.24]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) [spring-web-5.3.24.jar!/:5.3.24]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) [tomcat-embed-core-9.0.70.jar!/:na]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) [tomcat-embed-core-9.0.70.jar!/:na]
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) [spring-web-5.3.24.jar!/:5.3.24]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) [spring-web-5.3.24.jar!/:5.3.24]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) [tomcat-embed-core-9.0.70.jar!/:na]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) [tomcat-embed-core-9.0.70.jar!/:na]
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:177) [tomcat-embed-core-9.0.70.jar!/:na]
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97) [tomcat-embed-core-9.0.70.jar!/:na]
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541) [tomcat-embed-core-9.0.70.jar!/:na]
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:135) [tomcat-embed-core-9.0.70.jar!/:na]
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) [tomcat-embed-core-9.0.70.jar!/:na]
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) [tomcat-embed-core-9.0.70.jar!/:na]
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:360) [tomcat-embed-core-9.0.70.jar!/:na]
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:399) [tomcat-embed-core-9.0.70.jar!/:na]
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) [tomcat-embed-core-9.0.70.jar!/:na]
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:891) [tomcat-embed-core-9.0.70.jar!/:na]
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1784) [tomcat-embed-core-9.0.70.jar!/:na]
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) [tomcat-embed-core-9.0.70.jar!/:na]
at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191) [tomcat-embed-core-9.0.70.jar!/:na]
at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) [tomcat-embed-core-9.0.70.jar!/:na]
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-9.0.70.jar!/:na]
at java.lang.Thread.run(Thread.java:750) [na:1.8.0_412]
Caused by: org.apache.ibatis.exceptions.PersistenceException:
### Error querying database. Cause: org.springframework.jdbc.CannotGetJdbcConnectionException: Failed to obtain JDBC Connection; nested exception is com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure

The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server.
### The error may exist in com/common/server/mapper/UserInfoMapper.java (best guess)
### The error may involve com.common.server.mapper.UserInfoMapper.selectCount
### The error occurred while executing a query
### Cause: org.springframework.jdbc.CannotGetJdbcConnectionException: Failed to obtain JDBC Connection; nested exception is com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure

The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server.
at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:30) ~[mybatis-3.5.6.jar!/:3.5.6]
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:149) ~[mybatis-3.5.6.jar!/:3.5.6]
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:140) ~[mybatis-3.5.6.jar!/:3.5.6]
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectOne(DefaultSqlSession.java:76) ~[mybatis-3.5.6.jar!/:3.5.6]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_412]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_412]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_412]
at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_412]
at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:426) ~[mybatis-spring-2.0.5.jar!/:2.0.5]
... 70 common frames omitted
Caused by: org.springframework.jdbc.CannotGetJdbcConnectionException: Failed to obtain JDBC Connection; nested exception is com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure

The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server.
at org.springframework.jdbc.datasource.DataSourceUtils.getConnection(DataSourceUtils.java:83) ~[spring-jdbc-5.3.24.jar!/:5.3.24]
at org.mybatis.spring.transaction.SpringManagedTransaction.openConnection(SpringManagedTransaction.java:80) ~[mybatis-spring-2.0.5.jar!/:2.0.5]
at org.mybatis.spring.transaction.SpringManagedTransaction.getConnection(SpringManagedTransaction.java:67) ~[mybatis-spring-2.0.5.jar!/:2.0.5]
at org.apache.ibatis.executor.BaseExecutor.getConnection(BaseExecutor.java:337) ~[mybatis-3.5.6.jar!/:3.5.6]
at com.baomidou.mybatisplus.core.executor.MybatisSimpleExecutor.prepareStatement(MybatisSimpleExecutor.java:93) ~[mybatis-plus-core-3.4.2.jar!/:3.4.2]
at com.baomidou.mybatisplus.core.executor.MybatisSimpleExecutor.doQuery(MybatisSimpleExecutor.java:68) ~[mybatis-plus-core-3.4.2.jar!/:3.4.2]
at org.apache.ibatis.executor.BaseExecutor.queryFromDatabase(BaseExecutor.java:325) ~[mybatis-3.5.6.jar!/:3.5.6]
at org.apache.ibatis.executor.BaseExecutor.query(BaseExecutor.java:156) ~[mybatis-3.5.6.jar!/:3.5.6]
at com.baomidou.mybatisplus.core.executor.MybatisCachingExecutor.query(MybatisCachingExecutor.java:165) ~[mybatis-plus-core-3.4.2.jar!/:3.4.2]
at com.baomidou.mybatisplus.core.executor.MybatisCachingExecutor.query(MybatisCachingExecutor.java:92) ~[mybatis-plus-core-3.4.2.jar!/:3.4.2]
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:147) ~[mybatis-3.5.6.jar!/:3.5.6]
... 77 common frames omitted
Caused by: com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure

The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server.
at com.mysql.cj.jdbc.exceptions.SQLError.createCommunicationsException(SQLError.java:174) ~[mysql-connector-j-8.0.31.jar!/:8.0.31]
at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:64) ~[mysql-connector-j-8.0.31.jar!/:8.0.31]
at com.mysql.cj.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:828) ~[mysql-connector-j-8.0.31.jar!/:8.0.31]
at com.mysql.cj.jdbc.ConnectionImpl.<init>(ConnectionImpl.java:448) ~[mysql-connector-j-8.0.31.jar!/:8.0.31]
at com.mysql.cj.jdbc.ConnectionImpl.getInstance(ConnectionImpl.java:241) ~[mysql-connector-j-8.0.31.jar!/:8.0.31]
at com.mysql.cj.jdbc.NonRegisteringDriver.connect(NonRegisteringDriver.java:198) ~[mysql-connector-j-8.0.31.jar!/:8.0.31]
at com.zaxxer.hikari.util.DriverDataSource.getConnection(DriverDataSource.java:138) ~[HikariCP-4.0.3.jar!/:na]
at com.zaxxer.hikari.pool.PoolBase.newConnection(PoolBase.java:364) ~[HikariCP-4.0.3.jar!/:na]
at com.zaxxer.hikari.pool.PoolBase.newPoolEntry(PoolBase.java:206) ~[HikariCP-4.0.3.jar!/:na]
at com.zaxxer.hikari.pool.HikariPool.createPoolEntry(HikariPool.java:476) ~[HikariCP-4.0.3.jar!/:na]
at com.zaxxer.hikari.pool.HikariPool.checkFailFast(HikariPool.java:561) ~[HikariCP-4.0.3.jar!/:na]
at com.zaxxer.hikari.pool.HikariPool.<init>(HikariPool.java:115) ~[HikariCP-4.0.3.jar!/:na]
at com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java:112) ~[HikariCP-4.0.3.jar!/:na]
at org.springframework.jdbc.datasource.DataSourceUtils.fetchConnection(DataSourceUtils.java:159) ~[spring-jdbc-5.3.24.jar!/:5.3.24]
at org.springframework.jdbc.datasource.DataSourceUtils.doGetConnection(DataSourceUtils.java:117) ~[spring-jdbc-5.3.24.jar!/:5.3.24]
at org.springframework.jdbc.datasource.DataSourceUtils.getConnection(DataSourceUtils.java:80) ~[spring-jdbc-5.3.24.jar!/:5.3.24]
... 87 common frames omitted
Caused by: com.mysql.cj.exceptions.CJCommunicationsException: Communications link failure

The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server.
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) ~[na:1.8.0_412]
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) ~[na:1.8.0_412]
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) ~[na:1.8.0_412]
at java.lang.reflect.Constructor.newInstance(Constructor.java:423) ~[na:1.8.0_412]
at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:61) ~[mysql-connector-j-8.0.31.jar!/:8.0.31]
at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:105) ~[mysql-connector-j-8.0.31.jar!/:8.0.31]
at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:151) ~[mysql-connector-j-8.0.31.jar!/:8.0.31]
at com.mysql.cj.exceptions.ExceptionFactory.createCommunicationsException(ExceptionFactory.java:167) ~[mysql-connector-j-8.0.31.jar!/:8.0.31]
at com.mysql.cj.protocol.a.NativeSocketConnection.connect(NativeSocketConnection.java:89) ~[mysql-connector-j-8.0.31.jar!/:8.0.31]
at com.mysql.cj.NativeSession.connect(NativeSession.java:120) ~[mysql-connector-j-8.0.31.jar!/:8.0.31]
at com.mysql.cj.jdbc.ConnectionImpl.connectOneTryOnly(ConnectionImpl.java:948) ~[mysql-connector-j-8.0.31.jar!/:8.0.31]
at com.mysql.cj.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:818) ~[mysql-connector-j-8.0.31.jar!/:8.0.31]
... 100 common frames omitted
Caused by: java.net.ConnectException: 拒绝连接 (Connection refused)
at java.net.PlainSocketImpl.socketConnect(Native Method) ~[na:1.8.0_412]
at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350) ~[na:1.8.0_412]
at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206) ~[na:1.8.0_412]
at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188) ~[na:1.8.0_412]
at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392) ~[na:1.8.0_412]
at java.net.Socket.connect(Socket.java:607) ~[na:1.8.0_412]
at com.mysql.cj.protocol.StandardSocketFactory.connect(StandardSocketFactory.java:153) ~[mysql-connector-j-8.0.31.jar!/:8.0.31]
at com.mysql.cj.protocol.a.NativeSocketConnection.connect(NativeSocketConnection.java:63) ~[mysql-connector-j-8.0.31.jar!/:8.0.31]
... 103 common frames omitted

明天再看吧,现在头脑有点迷糊。

image-20241226180218663

2024 年 12 月 27 日

果然还是域名解析的问题,在我本机和虚拟机远程连接的 Linux 服务器上启动项目时都出现了这样的问题。

1
2
3
4
5
# MySQL
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://192.168.118.118:3306/iois_backend?zeroDateTimeBehavior=convertToNull
spring.datasource.username=root
spring.datasource.password=root

image-20241227084235055

这下接口文档正常运行了。

不过这毕竟不是长久之计,还是得研究下怎么解决配置 DNS 解析域名失效的问题。

Linux 无法正常解析域名_linux 无法解析域名-CSDN博客

linux服务器域名解析失败解决_域名解析暂时失败 linux-CSDN博客

linux无法解析域名怎么办_kelukelu1的技术博客_51CTO博客

全局异常处理

2024 年 12 月 26 日

微服务架构定义全局异常处理(@ControllerAdvice + @ExceptionHandler)没有生效_微服务架构base模块设置了全局异常处理器,为什么其他服务捕获不到-CSDN博客

特么不会这么长时间以来,分模块开发下的全局异常处理从来就没有生效过。。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 全局异常处理器
*/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

@ExceptionHandler(BusinessException.class)
public BaseResponse<?> businessExceptionHandler(BusinessException e) {
log.error("BusinessException", e);
return ResultUtils.error(e.getCode(), e.getMessage());
}

@ExceptionHandler(RuntimeException.class)
public BaseResponse<?> runtimeExceptionHandler(RuntimeException e) {
log.error("RuntimeException", e);
return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "系统错误");
}
}
1
2
3
4
5
6
7
@SpringBootApplication(scanBasePackages = {"com.common.server", "com.backend.server"})
@MapperScan("com.common.server.mapper")
public class BackendServerApplication {
public static void main(String[] args) {
SpringApplication.run(BackendServerApplication.class, args);
}
}

过滤器

2024 年 12 月 30 日

springboot实现过滤器_springboot 过滤器-CSDN博客

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Component
@Order(2)
public class WebFilter implements Filter {

@Override
public void init(FilterConfig filterConfig) throws ServletException {
// 过滤器初始化逻辑(可选)
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {

HttpServletRequest httpRequest = (HttpServletRequest) request;
System.out.println("请求的URI: " + httpRequest.getRequestURI());

// 继续执行下一个过滤器或请求处理
chain.doFilter(request, response);

System.out.println("响应已发送");
}

@Override
public void destroy() {
// 过滤器销毁逻辑(可选)
}
}

Swagger访问路径添加前缀_swagger prefix-CSDN博客

过滤器(filter)和拦截器(Interceptor)的区别以及使用场景_过滤器和拦截器的区别和使用场景-CSDN博客

我有个想法,今天下午实践一番。

使用拦截器替代过滤器。

2025 年 1 月 2 日

面试突击90:过滤器和拦截器有什么区别?持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第3天,点 - 掘金 (juejin.cn)

公共属性字段

2025 月 1 月 2 日

逻辑删除:

1
2
3
4
5
6
7
8
9
10
11
# MybatisPlus
mybatis-plus:
configuration:
map-underscore-to-camel-case: false
# log
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
logic-delete-field: isDelete
logic-delete-value: 1
logic-not-delete-value: 0

或者:

1
2
3
4
5
6
# Mybatis-Plus
mybatis-plus.configuration.map-underscore-to-camel-case=true
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
mybatis-plus.global-config.db-config.logic-delete-field=is_deleted
mybatis-plus.global-config.db-config.logic-delete-value=1
mybatis-plus.global-config.db-config.logic-not-delete-value=0

确保你的实体类上使用了 @TableLogic 注解来标记逻辑删除字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 是否删除
*/
@ApiModelProperty(value = "是否删除")
@TableLogic
private Integer isDeleted;

/**
* 创建时间
*/
@ApiModelProperty(value = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;

/**
* 修改时间
*/
@ApiModelProperty(value = "修改时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date updateTime;

公共字段。

一文带你掌握MyBatis-Plus的plus高级功能点如何使用 (qq.com)

Springboot 实现公共字段填充问题分析 方式一:自定义注解AutoFill 创建注解 创建枚举 创建切面类 ma - 掘金 (juejin.cn)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
* 库表字段公共属性
*/
@Data
public class BaseDO implements Serializable {

/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
@ApiModelProperty(value = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;

/**
* 修改时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
@ApiModelProperty(value = "修改时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date updateTime;

/**
* 创建人
*/
@TableField(fill = FieldFill.INSERT)
@ApiModelProperty(value = "创建人")
private Long createBy;

/**
* 修改人
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
@ApiModelProperty(value = "修改人")
private Long updateBy;

/**
* 是否删除
*/
@ApiModelProperty(value = "是否删除")
@TableLogic
private Integer isDeleted;
}

@Bulider 注解,出现这个报错:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
void addUser() {
UserInfo userInfo = UserInfo.builder().id(100L)
.userAccount("user2")
.userPassword("12345678")
.userName("user2")
.userAvatar("头像")
.userProfile("无简介")
.userRole("user")
.build();
boolean save = userInfoService.save(userInfo);
System.out.println(save + " " + userInfo);
}
1
2
3
4
java: 无法将类 com.common.server.entity.user.UserInfo中的构造器 UserInfo应用到给定类型;
需要: java.lang.Long,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String
找到: 没有参数
原因: 实际参数列表和形式参数列表长度不同

image-20250102143356720

加个无参构造函数即可。

艹,还是不管用,这两种构造方法只能用其中一个。

特么出现这个报错:

1
aused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'licenseCreateServiceImpl': Injection of resource dependencies failed; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'userInfoServiceImpl': Unsatisfied dependency expressed through field 'baseMapper'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'userInfoMapper' defined in file [D:\Project\tellhow\iois-backend\common-server\target\classes\com\common\server\mapper\UserInfoMapper.class]: Unsatisfied dependency expressed through bean property 'sqlSessionFactory'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'sqlSessionFactory' defined in class path resource [com/baomidou/mybatisplus/autoconfigure/MybatisPlusAutoConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.apache.ibatis.session.SqlSessionFactory]: Factory method 'sqlSessionFactory' threw exception; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'defaultMetaObjectHandler' defined in class path resource [com/common/server/config/MybatisPlusConfig.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.baomidou.mybatisplus.core.handlers.MetaObjectHandler]: Factory method 'defaultMetaObjectHandler' threw exception; nested exception is java.lang.ExceptionInInitializerError

image-20250102150458907

分析:

ExceptionInInitializerError

  • 这个错误通常表明在静态初始化块或静态变量初始化时发生了异常。在您的 DefaultDBFieldHandler 类(假设这是您自定义的 MetaObjectHandler 实现)中,可能有一个静态初始化块或静态变量初始化时抛出了异常。
  • 检查 DefaultDBFieldHandler 类中的静态代码块和静态变量初始化代码,确保没有抛出任何未捕获的异常。

直接写这里边吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 获取当前登录用户
*
* @return
*/
private UserInfo getCurrentUser() {
// 先判断是否已登录
HttpServletRequest request =
((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder
.getRequestAttributes())).getRequest();
UserInfo currentUser = (UserInfo) request.getSession().getAttribute(UserConstant.USER_LOGIN_STATE);
ThrowUtils.throwIf(ObjectUtils.isEmpty(currentUser), ErrorCode.NOT_LOGIN_ERROR, "用户未登录");
return currentUser;
}

有新问题了:

1
org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.reflection.ReflectionException: Could not set property 'create_time' of 'class com.common.server.entity.user.UserInfo' with value '2025-01-02T15:08:35.873751' Cause: org.apache.ibatis.reflection.ReflectionException: There is no setter for property named 'create_time' in 'class com.common.server.entity.user.UserInfo'

image-20250102150939067

image-20250102151743490

排查出来了,上面是数据库的问题。

1
org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.reflection.ReflectionException: Could not set property 'createBy' of 'class com.common.server.entity.user.UserInfo' with value 'UserInfo(id=10, userAccount=admin, userPassword=cc7554766460d0192185d2b43f0acc69, userName=admin, userAvatar=, userProfile=暂无简介, userRole=admin, isDeleted=0)' Cause: java.lang.IllegalArgumentException: argument type mismatch

艹,特么应该填充当前登录用户的 id 的。

1
2
3
4
// 填充创建人信息
metaObject.setValue("createBy", currentUser.getId());
// 填充更新人信息
metaObject.setValue("updateBy", currentUser.getId());

很显然成功了,公共字段填充。

总结下具体流程:

定义库表字段公共属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* 库表字段公共属性
*/
@Data
public class BaseEntity implements Serializable {
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
@ApiModelProperty(value = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime createTime;

/**
* 修改时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
@ApiModelProperty(value = "修改时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime updateTime;

/**
* 创建人
*/
@TableField(fill = FieldFill.INSERT)
@ApiModelProperty(value = "创建人")
private Long createBy;

/**
* 修改人
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
@ApiModelProperty(value = "修改人")
private Long updateBy;
}

公共属性字段填充:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
**
* 公共属性字段值填充
*/
public class DefaultDBFieldHandler implements MetaObjectHandler {

@Override
public void insertFill(MetaObject metaObject) {
if (Objects.nonNull(metaObject) && metaObject.getOriginalObject() instanceof BaseEntity) {
// 填充创建时间
metaObject.setValue("createTime", LocalDateTime.now());
// 填充修改时间
metaObject.setValue("updateTime", LocalDateTime.now());
// 获取当前登录用户
UserInfo currentUser = getCurrentUser();
// 填充创建人信息
metaObject.setValue("createBy", currentUser.getId());
// 填充更新人信息
metaObject.setValue("updateBy", currentUser.getId());
}
}

@Override
public void updateFill(MetaObject metaObject) {
// 填充更新时间
metaObject.setValue("updateTime", LocalDateTime.now());
// 获取当前登录用户
UserInfo currentUser = getCurrentUser();
// 填充修改人信息
metaObject.setValue("updateBy", currentUser.getId());
}

/**
* 获取当前登录用户
*
* @return
*/
private UserInfo getCurrentUser() {
// 先判断是否已登录
HttpServletRequest request =
((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder
.getRequestAttributes())).getRequest();
UserInfo currentUser = (UserInfo) request.getSession().getAttribute(UserConstant.USER_LOGIN_STATE);
ThrowUtils.throwIf(ObjectUtils.isEmpty(currentUser), ErrorCode.NOT_LOGIN_ERROR, "用户未登录");
return currentUser;
}
}

实体类集成公共属性类:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 用户
*
* @TableName user
*/
@EqualsAndHashCode(callSuper = true)
@TableName(value = "user_info")
@ApiModel(value = "用户信息")
@Data
public class UserInfo extends BaseEntity implements Serializable {

.......................
}

Mybatis-Plus 新增自动填充插件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Configuration
public class MybatisPlusConfig {
/**
* 新增分页拦截器,并设置数据库类型为mysql
*/
@Bean(name = "mybatisPlusInterceptor")
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}

/**
* 新增自动填充插件
*/
@Bean
public MetaObjectHandler defaultMetaObjectHandler() {
return new DefaultDBFieldHandler();
}
}

最后测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
void addUser() {
HttpServletRequest request =
((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder
.getRequestAttributes())).getRequest();
// 登录
userInfoService.userLogin("admin", "12345678", request);
// 新增用户
UserInfo userInfo = new UserInfo();
userInfo.setId(100L);
userInfo.setUserAccount("user2");
userInfo.setUserPassword("12345678");
userInfo.setUserName("user2");
userInfo.setUserAvatar("头像");
userInfo.setUserProfile("无简介");
userInfo.setUserRole("user");
userInfoService.save(userInfo);
boolean save = userInfoService.save(userInfo);
System.out.println(save + " " + userInfo);
}

那么用户注册时,又会怎么填充呢。

1
2
3
4
5
6
@Test
void register() {
// 注册
long register = userInfoService.userRegister("user3", "12345678", "12345678");
System.out.println(register);
}

所以最后完善下吧,默认填充操作人以及修改人为当前登录用户 id,否则填充管理员 id。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Override
public void insertFill(MetaObject metaObject) {
if (Objects.nonNull(metaObject) && metaObject.getOriginalObject() instanceof BaseEntity) {
// 填充创建时间
metaObject.setValue("createTime", LocalDateTime.now());
// 填充修改时间
metaObject.setValue("updateTime", LocalDateTime.now());
// 获取当前登录用户
UserInfo currentUser = getCurrentUser();
if (Objects.nonNull(currentUser)) {
// 填充创建人信息
metaObject.setValue("createBy", currentUser.getId());
metaObject.setValue("updateBy", currentUser.getId());
} else {
metaObject.setValue("createBy", 1L);
metaObject.setValue("updateBy", 1L);
}
}
}

@Override
public void updateFill(MetaObject metaObject) {
// 填充更新时间
metaObject.setValue("updateTime", LocalDateTime.now());
// 获取当前登录用户
UserInfo currentUser = getCurrentUser();
// 填充修改人信息
if (Objects.nonNull(currentUser)) {
metaObject.setValue("updateBy", currentUser.getId());
} else {
metaObject.setValue("updateBy", 1L);
}
}

61850

2024 年 12 月 27 日

61850 突击队,拉群。

2025 年 1 月 3 日

[IEC 61850_百度百科 (baidu.com)](https://baike.baidu.com/item/IEC 61850/9210067)

解析IEC 61850通信规约_61850通讯规约-CSDN博客

IEC61850规约详细解读.pdf-原创力文档 (book118.com)

61850开发知识总结与分享【1】(包含自己从整个互联网搜索到的所有61850资源)-CSDN博客

困死嘞。

看着东西简直吃力不讨好,但是这几篇博客看下来,对这个协议的理解又增添了几分。

IEC61850 Server Simulator - IEC61850 服务端模拟器 (redisant.cn)

image-20250103093913490

IEC61850标准技术介绍工程调试版 - 百度文库 (baidu.com)

变电站_百度百科 (baidu.com)

1
2
3
4
1、一次设备
一次设备指直接生产、输送、分配和使用电能的设备,主要包括变压器、高压断路器、隔离开关、母线、避雷器、电容器、电抗器等。
2、二次设备
变电站的二次设备是指对一次设备和系统的运行工况进行测量、监视、控制和保护的设备,它主要由包括继电保护装置、自动装置、测控装置、计量装置、自动化系统以及为二次设备提供电源的直流设备。 [2]

快速了解IEC61850 标准-CSDN博客

2025 年 1 月 13 日

早十点五十分,通知明天下班前拉个会,讨论下这个项目目前进展。

主要可以学习推进的进度,只有简单看下文档。

这两天可以看看服务端模拟器和客户端模拟器,学习下怎么跑通两端联调,在基础知识理解清楚的前提下调试会简单很多。

今天下班前做这个事。

拉取项目代码,构建 Maven 工程:JAVA版IEC61850实例:电力通信协议的JAVA实现-CSDN博客

1
2
3
4
5
6
7
8
9
10
11
12
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>/
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<!-- 可选的配置 -->
</configuration>
</plugin>
</plugins>
</build>

2025 年 1 月 14 日

昨天临时整改 License 证书校验功能,下午没空学这个 61850。

今上午快吃饭这会儿看看代码,构建 Maven 工程之后依赖坐标什么的都没有导入,项目爆红一大片。

估计整不了了。

艹。

下午开个小会。

这几个依赖导入一下,不过 Maven 中央仓库可能拉不下来,也许是阿里云镜像缺失呢,又或者版本啥的太过老旧。

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>org.openmuc</groupId>
<artifactId>iec61850-client</artifactId>
<version>1.4.3</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.ling5821.iot/protocol-iec61850-core -->
<dependency>
<groupId>com.ling5821.iot</groupId>
<artifactId>protocol-iec61850-core</artifactId>
<version>0.0.6</version>
</dependency>

2025 年 1 月 17 日

IEC61850bean/OpenIEC61850 用户指南 |豆 (beanit.com)

61850开发知识总结与分享【1】(包含自己从整个互联网搜索到的所有61850资源)-CSDN博客

连接服务端。

实用的 IEC61850 装置设备模拟器 - serene1312 - 博客园 (cnblogs.com)

下午五点半,计东给发了最新的解析 IEC61850 的开源项目代码,可以拿取到。

image-20250117173707229

代码统计

2025 年 1 月 7 日

【开发工具】统计项目代码的总行数_idea 统计代码行数-CSDN博客

别的不说,这几行命令是去年秋招那会儿填写资料时学会的,要求在项目目录下执行以下命令并截图保存,体现代码量。

1
git log --author="memory" --pretty=tformat: --numstat | awk '{ add += $1; subs += $2; loc += $1 - $2 } END { printf "added lines: %s, removed lines: %s, total lines: %s\n", add, subs, loc }';git shortlog --all --numbered --summary --no-merges

image-20250107164518492

image-20250107165413223

新项目,代码量不少。

1
2
3
4
5
6
7
8
# 后端代码统计
$ find . "(" -name "*.java" -or -name "*.jsp" -or -name "*.js" -or -name "*.xml" ")" -print | xargs grep -v "^$" | wc -l

# 前端代码统计
$ find . "(" -name "*.html" -or -name "*.js" -or -name "*.css" -or -name "*.vue" ")" -print | xargs grep -v "^$" | wc -l

# c#项目代码统计
$ find . "(" -name "*.cs" -or -name "*.resx" ")" -print | xargs grep -v "^$" | wc -l

image-20250108092946684

image-20250108092852104

周例会

2025 年 1 月 9 日

1
@所有人 近期有发现tfs上生产分支代码和生产环境实际部署的应用不一致的问题。 现在本周开始代码自查,没有提交tfs的抓紧提交。 下周开始互查和随机抽查, 一经发现代码不一致,发现一处扣绩效1000元。发现多处的,记录下来,后面分多个月扣除,每个月扣一次。(参与检查的项目范围为2024年研发参与的所有项目,包括上半年已经交付的项目)
1
@所有人 tfs任务记得及时关闭,上周少关的任务及时补上。明天例会就tfs上生产分支代码和生产环境实际部署的应用不一致的问题请每个人都准备好发言意见。

image-20250109160347682

配置文件

2025 年 1 月 14 日

这个破问题折磨我一年多了,总是彻底解决不了。

jar启动报错:org.yaml.snakeyaml.error.YAMLException: java.nio.charset.MalformedInputExcept:Input length =-CSDN博客

maven常用三种打包插件_maven打包插件-CSDN博客

运行jar包提示 “XXX中没有主清单属性” “找不到主类”两种解决办法-CSDN博客

2025 年 2 月 7 日

Java 新建yaml文件如何获取配置-CSDN博客

java如何获取yml配置文件工具类_java_脚本之家 (jb51.net)

怎么都是 snakeYaml 依赖。。

传参。不需要再次读取配置文件信息了。

1
2
3
4
5
6
// 默认证书文件路径
String defaultFileName = String.join("", LicenseConstant.DEFAULT_LICENSE_FILE, LicenseConstant.LICENSE_FILE_EXTENSION);
String defaultLicensePath = Paths.get(LicenseConstant.USER_DIR, this.licensePath, defaultFileName).toString();
param.setDefaultLicensePath(defaultLicensePath); // 默认证书路径
// 执行安装
return new LicenseVerify().install(param);

脚本

2025 年 1 月 22 日

Java中jar包运行后显示:没有主清单属性的解决方案_java_脚本之家 (jb51.net)

java springboot 获取cpu编号 java 获取cpu序列号_mob64ca1401b651的技术博客_51CTO博客

艹。

image-20250122152922367

  1. 配置静态 IP 地址
    在打开的配置文件中,找到或添加以下行来设置静态 IP 地址、子网掩码、网关和 DNS 服务器:

    1
    2
    3
    4
    5
    6
    BOOTPROTO=static
    IPADDR=<您的静态IP地址>
    NETMASK=<您的子网掩码>
    GATEWAY=<您的网关地址>
    DNS1=<您的DNS服务器地址1>
    DNS2=<您的DNS服务器地址2>(可选)

    替换 <您的静态IP地址><您的子网掩码><您的网关地址><您的DNS服务器地址1> 等占位符为实际的 IP 地址值。

  2. 保存并退出编辑器
    vim 中,您可以按 Esc 键,然后输入 :wq 并按回车来保存更改并退出编辑器。

  3. 重启网络服务
    使用以下命令重启网络服务以应用更改:

    1
    2
    3
    bash复制代码

    sudo systemctl restart network

ping不通Linux服务器的原因?_windows ping不通linux-CSDN博客

如何排查和解决Linux服务器配置IP后Ping不通的问题?-桔子数据 (95vps.com)

image-20250122164448465

网络服务没在正常运行?

艹。

1
Job for network.service failed because the control process exited with error code. See "systemctl status network.service" and "journalctl -xe" for details.
1
2
3
4
5
6
7
8
9
10
private LicenseCheckModel hardwareInfo;

public CustomLicenseManager() {

}

public CustomLicenseManager(LicenseParam param, LicenseCheckModel hardwareInfo) {
super(param);
this.hardwareInfo = hardwareInfo;
}

2025 年 1 月 23 日

1
2
3
4
5
6
7
8
9
10
11
// 现场服务器硬件信息审核
Gson gson = new Gson();
// 获取证书包含的硬件信息
LicenseCheckModel checkInfo = gson.fromJson(content.getExtra().toString(), LicenseCheckModel.class);
// 服务端不需要校验现场服务器硬件信息
// LicenseCheckModel hardwareInfo = HardwareInfoContext.getHardwareInfo();
// Boolean checked = checkHardwareInfo(hardwareInfo, checkInfo);
// if (!checked) {
// log.error("现场服务器硬件信息审核不通过");
// throw new BusinessException(401, "现场服务器硬件信息审核不通过");
// }
1
2
3
4
5
6
7
8
9
10
11
12
13

private static volatile LicenseManager LICENSE_MANAGER;

public static LicenseManager getInstance(LicenseParam param) {
if (LICENSE_MANAGER == null) {
synchronized (LicenseManagerHolder.class) {
if (LICENSE_MANAGER == null) {
LICENSE_MANAGER = new LicenseManager(param);
}
}
}
return LICENSE_MANAGER;
}

后台直接写两个安装逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* @param param 证书安装需要的参数
* @return LicenseContent
*/
public synchronized LicenseContent install(LicenseVerifyParam param) {
log.info("++++++++ 开始安装证书 ++++++++");
LicenseContent result = null;
DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 安装证书
try {
LicenseParam licenseParam = initLicenseParam(param);
LicenseManager licenseManager = LicenseManagerHolder.getInstance(licenseParam);
// 删除旧证书
licenseManager.uninstall();
result = licenseManager.install(new File(param.getLicensePath()));
log.info("证书安装成功");
log.info(MessageFormat.format("证书安装成功,证书有效期:{0} - {1}", format.format(result.getNotBefore()), format.format(result.getNotAfter())));
log.info("证书内容:{}", result);
} catch (Exception e) {
log.error("证书安装失败!", e);
throw new CommonException(401, "证书安装失败");
// System.exit(0);
}
log.info("++++++++ 证书安装结束 ++++++++");
// 证书校验
result.setExtra(result.getExtra());
return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* @param param 证书安装需要的参数
* @return LicenseContent
*/
public synchronized LicenseContent install(LicenseVerifyParam param, LicenseCheckModel hardwareInfo) {
log.info("++++++++ 开始安装证书 ++++++++");
LicenseContent result = null;
DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 安装证书
try {
LicenseParam licenseParam = initLicenseParam(param);
LicenseManager licenseManager = LicenseManagerHolder.getInstance(licenseParam);
// 删除旧证书
licenseManager.uninstall();
// 现场服务器硬件信息
HardwareInfoContext.setHardwareInfo(hardwareInfo);
// result = licenseManager.install(new File(param.getLicensePath()));
result = new CustomLicenseManager(licenseParam).install(new File(param.getLicensePath()));
log.info("证书安装成功");
log.info(MessageFormat.format("证书安装成功,证书有效期:{0} - {1}", format.format(result.getNotBefore()), format.format(result.getNotAfter())));
log.info("证书内容:{}", result);
} catch (Exception e) {
log.error("证书安装失败!", e);
throw new CommonException(401, "证书安装失败");
// System.exit(0);
}
log.info("++++++++ 证书安装结束 ++++++++");
// 证书校验
result.setExtra(result.getExtra());
return result;
}

经测试,可以保存传输过来的硬件信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 安装证书
try {
LicenseParam licenseParam = initLicenseParam(param);
LicenseManager licenseManager = LicenseManagerHolder.getInstance(licenseParam);
// 删除旧证书
licenseManager.uninstall();
LicenseCheckModel licenseCheckModel = new LicenseCheckModel();
licenseCheckModel.setCpuSerial("sauwiqurh");
licenseCheckModel.setMainBoardSerial("sauwiqurh");
HardwareInfoContext.setHardwareInfo(licenseCheckModel);
// result = licenseManager.install(new File(param.getLicensePath()));
result= new CustomLicenseManager(licenseParam).install(new File(param.getLicensePath()));
log.info("证书安装成功");
log.info(MessageFormat.format("证书安装成功,证书有效期:{0} - {1}", format.format(result.getNotBefore()), format.format(result.getNotAfter())));
log.info("证书内容:{}", result);
} catch (Exception e) {
log.error("证书安装失败!", e);
throw new BusinessException(ErrorCode.OPERATION_ERROR, "证书安装失败");
// System.exit(0);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class HardwareInfoContext {
private static final ThreadLocal<LicenseCheckModel> threadLocal = new ThreadLocal<>();

public static void setHardwareInfo(LicenseCheckModel hardwareInfo) {
threadLocal.set(hardwareInfo);
}

public static LicenseCheckModel getHardwareInfo() {
return threadLocal.get();
}

public static void clear() {
threadLocal.remove();
}
}

2025 年 2 月 5 日

补一下假期里学过的四条命令,分别用来获取本机 CPU 序列号,主板序列号,IP 地址,MAC 地址。

1
sudo dmidecode -t processor | grep 'ID' | awk -F ':' '{print $2}' | head -n 1
1
sudo dmidecode | grep 'Serial Number' | awk -F ':' '{print $2}' | head -n 1
1
ifconfig | grep -Eo 'inet (addr:)?([0-9]*\.){3}[0-9]*' | grep -Eo '([0-9]*\.){3}[0-9]*' | grep -v '127.0.0.1'
1
ip link show | awk '/ether/ {match($0, /ether ([0-9a-fA-F:]+)/, arr); print $2, arr[1]}'

Matlab

2025 年 2 月 11 日

下午一点半多钟,阳哥拉我进群搞 matlab 数据分析,下载破解版软件是第一步。

零基础入门Matlab(一篇两个小时就能学完的入门博客)-CSDN博客

手把手教你安装matlab软件_matlab安装教程-CSDN博客

这两篇可行:

在JAVA开发中调用matlab程序_java调用matlab程序-CSDN博客

Matlab代码打包成jar包供java调用_marleb算法 打成jar与java交互怎么使用-CSDN博客

1
做了一个优化算法可视化系统,但优化的过程需要使用matlab来计算,因此需要在项目里引入matlab封装好的jar包,并在java开发中调用matlab方法,指定输入参数,转换输出类型。

2025年了,令人唏嘘的Angular,现在怎么样了🚀🚀🚀迅速崛起和快速退出 时间回到2014年,此时的 Angu - 掘金 (juejin.cn)

下午一点半开始下载 Matlab 安装包,一个半小时左右下载完成,解压安装。

三点半这会儿给同事用U盘传输安装包文件,到这会儿五点钟,还是百分之七十七。

下班前搞不完了么。

搞定。

2025 年 2 月 12 日

良心推荐 最适合新手学习的Matlab快速入门教程_matlab入门-CSDN博客

1
disp("hello world")

matlab入门教程二 —– 常用函数&矩阵基本操作&&数组基本操作_a(2)在matlab-CSDN博客

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
A = [1, 2, 3; 4, 5 ,6]; % 赋值 用分号隔开每一行,同一行中的元素用逗号或者空格隔开
A

%A(i,j) 表示矩阵 A 的第 i 行第 j 列元素
A(1, 1) %第一行第一列

%A( : ,j) 表示矩阵 A 的第 j 列
A(: , 2) %第二列

%A(i, : ) 表示矩阵 A 的第 i 行
A(2, : ) %第二行
% A( : , : ) 表示 A 的所有元素构造 2 维矩阵。
%A( : ) 表示以矩阵 A 的所有元素做成的一个列矩阵
A( : , : )
A( : )

A(1) %表示矩阵的第一个元素
A(2:4) % 表示取矩阵的第2到第4个元素,注意这里是按列数的
A(1,1) = 100; %表示把第一行第一列元素修改为100

A(3, 3) = 100; %表示把矩阵扩展为三行三列且把第三行第三列元素改为100 ,其他为赋值元素默认为0
A
A(1, : ) = [ ] %删除矩阵 A 的第一行
A
A(2: 2) = 666;
A

编写 Java 代码调用 matlab 接口实现数据分析处理,首先得搞明白 matlab 文件运行情况。

很简单的操作。

Matlab 安装

昨天已经下载解压安装完毕。

百度网盘 - 下载链接

各版本安装教程

解压密码:yiliu。

MCR 安装

matlab mcr安装图标,Matlab运行环境MCR安装-CSDN博客

在安装路径中获取MCR。

进入 matlab 输入 mcrinstaller,会弹出 mcr 的路径。

1
mcrinstaller
1
2
>> compiler.runtime.download
Downloading MATLAB Runtime installer. It may take several minutes...

正在安装中……

1
2
3
4
5
6
>> compiler.runtime.download
Downloading MATLAB Runtime installer. It may take several minutes...


MATLAB Runtime installer has been downloaded to:
"C:\Users\Lenovo\AppData\Local\Temp\Lenovo\MCRInstaller24.1\MATLAB_Runtime_R2024a_win64.zip"

接下来考虑下,这个过程中有安装 MCR 么,没有 Matlab 编译运行时环境还能不能跑这段代码?

特么当然用到了。

1
"D:\Project\tellhow\广西项目\test\doubleInput2\for_redistribution\MyAppInstaller_web.exe"

这是打包好 doubleInput 项目以后,在这个目录下找到了运行环境安装可执行文件,同我上面解压出来的运行环境安装包是一样的。

最终都在本机配置了相应的环境变量。

image-20250212153238183

不过很明显,后者要占用磁盘空间更大一些,估计这是未压缩版。

image-20250212153451726

那么如果是在 Linux 系统下,前者 .exe 可执行文件也许就不适用了,只能采用解压运行环境压缩包来配置环境,还需要手动配置环境变量。

不好意思看走眼了,两个都是 .exe 文件格式。

只好是继续上传压缩包至 Linux 上。

1
C:\Users\Lenovo\AppData\Local\Temp\Lenovo\MCRInstaller24.1

解压。

艹。

解压下来还是 .exe 可执行文件。。

Linux下MatlabCompilerRuntime的安装和使用-腾讯云开发者社区-腾讯云 (tencent.com)

Linux下MatlabCompilerRuntime的安装和使用-腾讯云开发者社区-腾讯云 (tencent.com)

运行环境:MATLAB Runtime - MATLAB Compiler - MATLAB (mathworks.cn)

在这里直接安装 Linux 运行环境。

1
sudo yum install unzip
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
[root@iZ2ze4hnl6pls28qt4w1ttZ matlab]# unzip MATLAB_Runtime_R2024a_Update_7_glnxa64.zip -d MATLAB_Runtime_R2024a
-bash: unzip: 未找到命令
[root@iZ2ze4hnl6pls28qt4w1ttZ matlab]# sudo yum install unzip

已加载插件:fastestmirror
Determining fastest mirrors
base | 3.6 kB 00:00:00
docker-ce-stable | 3.5 kB 00:00:00
epel | 4.3 kB 00:00:00
extras | 2.9 kB 00:00:00
updates | 2.9 kB 00:00:00
正在解决依赖关系
--> 正在检查事务
---> 软件包 unzip.x86_64.0.6.0-24.el7_9 将被 安装
--> 解决依赖关系完成

依赖关系解决

================================================================================================================================================================================================
Package 架构 版本 源 大小
================================================================================================================================================================================================
正在安装:
unzip x86_64 6.0-24.el7_9 updates 172 k

事务概要
================================================================================================================================================================================================
安装 1 软件包

总下载量:172 k
安装大小:369 k
Is this ok [y/d/N]: Exiting on user command
您的事务已保存,请执行:
yum load-transaction /tmp/yum_save_tx.2025-02-13.09-47.op59C9.yumtx 重新执行该事务
[root@iZ2ze4hnl6pls28qt4w1ttZ matlab]#

1
unzip MATLAB_Runtime_R2024a_Update_7_glnxa64.zip -d MATLAB_Runtime_R2024a
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(base) [root@thmn matlab]# ll
总用量 4644956
drwxr-xr-x 88 root root 360448 2月 12 16:30 archives
drwxr-xr-x 4 root root 4096 2月 12 16:31 bin
drwxr-xr-x 3 root root 4096 2月 12 16:30 cefclient
drwxr-xr-x 3 root root 4096 2月 12 16:30 extern
-r-xr-xr-x 1 root root 11147 11月 20 2023 install
drwxr-xr-x 2 root root 4096 2月 12 16:30 java
-r--r--r-- 1 root root 1844 9月 1 2021 matlabruntime_installer_input.txt
-r--r--r-- 1 root root 17018 4月 1 2022 matlabruntime_license_agreement.pdf
-rw-r--r-- 1 root root 4755989145 2月 12 16:26 MATLAB_Runtime_R2024a_Update_7_glnxa64.zip
drwxr-xr-x 2 root root 4096 2月 12 16:30 productdata
drwxr-xr-x 14 root root 4096 2月 12 16:30 resources
drwxr-xr-x 4 root root 4096 2月 12 16:31 sys
drwxr-xr-x 3 root root 4096 2月 12 16:30 ui
-r--r--r-- 1 root root 311 1月 3 06:29 VersionInfo.xml
1
./install -mode silent -agreeToLicense  yes
1
sudo vim /etc/profile
1
2
3
export LD_LIBRARY_PATH=/usr/local/MATLAB/MATLAB_Runtime/v90/runtime/glnxa64:$LD_LIBRARY_PATH
export LD_LIBRARY_PATH=/usr/local/MATLAB/MATLAB_Runtime/v90/bin/glnxa64:$LD_LIBRARY_PATH
export LD_LIBRARY_PATH=/usr/local/MATLAB/MATLAB_Runtime/v90/sys/os/glnxa64:$LD_LIBRARY_PATH
1
source /etc/profile
1
2
(base) [root@thmn analysis]# echo "$LD_LIBRARY_PATH"
/usr/local/MATLAB/MATLAB_Runtime/v90/sys/os/glnxa64:/usr/local/MATLAB/MATLAB_Runtime/v90/bin/glnxa64:/usr/local/MATLAB/MATLAB_Runtime/v90/runtime/glnxa64:/usr/local/ffmpeg/lib::/usr/local/cuda-11.1/lib64:/usr/local/cuda/lib64:/usr/local/cuda-11.1/lib64

如果打印出了则成功了,此时matlab常用的动态库就配置好了。

尝试运行 jar 包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
(base) [root@thmn analysis]# java -jar _test2.jar 
Exception in thread "main" java.lang.UnsatisfiedLinkError: Failed to find the required library libmwmclmcrrt.so.24.1 on java.library.path.
This library is typically installed along with MATLAB or the MATLAB Runtime. Its absence may indicate an issue with that installation or
the current path configuration, or a mismatch with the architecture of the Java interpreter on the path.
MATLAB Runtime version this component is attempting to use: 24.1.
Java interpreter architecture: glnxa64.

at com.mathworks.toolbox.javabuilder.internal.MCRConfiguration$ProxyLibraryDir.get(MCRConfiguration.java:195)
at com.mathworks.toolbox.javabuilder.internal.MCRConfiguration$ProxyLibraryDir.<clinit>(MCRConfiguration.java:205)
at com.mathworks.toolbox.javabuilder.internal.MCRConfiguration.getProxyLibraryDir(MCRConfiguration.java:210)
at com.mathworks.toolbox.javabuilder.internal.MCRConfiguration$MCRRoot.get(MCRConfiguration.java:64)
at com.mathworks.toolbox.javabuilder.internal.MCRConfiguration$MCRRoot.<clinit>(MCRConfiguration.java:76)
at com.mathworks.toolbox.javabuilder.internal.MCRConfiguration.getMCRRoot(MCRConfiguration.java:81)
at com.mathworks.toolbox.javabuilder.internal.MCRConfiguration$ModuleDir.<clinit>(MCRConfiguration.java:53)
at com.mathworks.toolbox.javabuilder.internal.MCRConfiguration.getModuleDir(MCRConfiguration.java:58)
at com.mathworks.toolbox.javabuilder.internal.MWMCR.<clinit>(MWMCR.java:1775)
at doubleInput2.DoubleInput2MCRFactory.newInstance(DoubleInput2MCRFactory.java:50)
at doubleInput2.DoubleInput2MCRFactory.newInstance(DoubleInput2MCRFactory.java:66)
at doubleInput2.Class1.<init>(Class1.java:63)
at org.tellhow.matlab.Main.test1(Main.java:23)
at org.tellhow.matlab.Main.main(Main.java:18)

安装失败了吧。

1
chmod +x ./install
1
2
3
4
5
6
7
8
9
安装:./install -mode silent -agreeToLicense  yes

当出现下面的字样的时候,就表示MCR安装成功了:

Exiting with status 0

End – Successful.

Finished

重新解压一遍。

1
./install -mode silent -agreeToLicense  yes
1
2
3
4
说明:
-mode silent 静默安装
-agreeToLicense yes 同意
-destinationFoler /tpsys/MATLAB 程序安装的路径,可自定义。

真特么见鬼了我操,怎么就安装不成功呢。

一个模子里刻出来的安装方案:

Linux系统安装MATLAB环境 - 知乎 (zhihu.com)

linux matlab runtime,Linux安装MATLAB Compiler Runtime操作-CSDN博客

Linux搭建环境_linux 检查 mcr是否安装成功-CSDN博客

matlab mcr安装图标,Matlab运行环境MCR安装-CSDN博客

Linux下MatlabCompilerRuntime的安装和使用-腾讯云开发者社区-腾讯云 (tencent.com)

这篇好歹有点眉目了:关于Linux下安装MATLAB Compiler Runtime(MCR) 所遇到的问题以及解决方法(以Ubuntu 16.04 为例)_failed to find the required library libmwmclmcrrt.-CSDN博客

2025 年 2 月 13 日

早上刚来再次尝试安装,仍然失败,换了版本也还是同样的问题。

尝试本地虚拟机和阿里云服务器安装,同时探究下 Matlab 工程文件打包成 Shell 脚本运行的可行性。

1
mcc -m doubleInput.m -o doubleInput
1
2
3
4
5
6
7
8
9
10
11
12
13
function y = doubleInput(x)
y = 2 * x;

% 提示用户点击任意键继续或关闭
disp('Press any key to continue or close...');
% 使用 pause 函数等待用户输入,但这里 input 更合适因为我们想要检测任意键
% 注意:input 函数在 Windows 上默认等待回车键,但我们可以使用 's' 选项来检测任意键
if ispc % 检查是否在 Windows 系统上
input('Press Enter to continue...', 's');
else
warning('On non-Windows systems, you may need to press Enter instead of any key.');
pause; % 在非 Windows 系统上,简单地暂停,因为没有直接的任意键检测
end

下午。

换个解压目录试试。

1
unzip MATLAB_Runtime_R2024a_Update_7_glnxa64.zip -d /opt/matlab_mcr
1
2
3
4
5
6
7
8
/opt/matlab_mcr/archives/3p/cuda_glnxa64_1700442928.enc:  write error (disk full?).  Continue? (y/n/^C) y
bad CRC 27070484 (should be e87645ff)
extracting: /opt/matlab_mcr/archives/3p/adobe_glyph_list_common_1700434896.json
/opt/matlab_mcr/archives/3p/adobe_glyph_list_common_1700434896.json: write error (disk full?). Continue? (y/n/^C) y
inflating: /opt/matlab_mcr/archives/3p/fastdds_glnxa64_1700442591.enc
/opt/matlab_mcr/archives/3p/fastdds_glnxa64_1700442591.enc: write error (disk full?). Continue? (y/n/^C) c^Hy

warning: /opt/matlab_mcr/archives/3p/fastdds_glnxa64_1700442591.enc is probably truncated

特么磁盘空间满了。

1
df -h

安装成功了啊,特么安装到哪个目录下了。

1
2
export MCR_HOME=/usr/local/MATLAB/MATLAB_Runtime/R2024a
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$MCR_HOME/runtime/glnxa64:$MCR_HOME/bin/glnxa64:$MCR_HOME/sys/os/glnxa64

成功了,终于成功了。

1
2
[root@iZ2ze4hnl6pls28qt4w1ttZ R2024a]# echo "$LD_LIBRARY_PATH"
/usr/local/MATLAB/MATLAB_Runtime/v90/sys/os/glnxa64:/usr/local/MATLAB/MATLAB_Runtime/v90/bin/glnxa64:/usr/local/MATLAB/MATLAB_Runtime/v90/runtime/glnxa64::/usr/local/MATLAB/MATLAB_Runtime/R2024a/runtime/glnxa64:/usr/local/MATLAB/MATLAB_Runtime/R2024a/bin/glnxa64:/usr/local/MATLAB/MATLAB_Runtime/R2024a/sys/os/glnxa64

解压出新问题了:

1
2
3
[root@iZ2ze4hnl6pls28qt4w1ttZ matlab]# java -jar _test2.jar 
Error: dl failure on line 894
Error: failed /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.412.b08-1.el7_9.x86_64/jre/lib/amd64/server/libjvm.so, because /usr/local/MATLAB/MATLAB_Runtime/R2024a/sys/os/glnxa64/libstdc++.so.6: undefined symbol: __cxa_thread_atexit_impl

这是编辑环境变量之前的执行结果:

1
2
3
4
(base) [root@thmn matlab]# java -version
openjdk version "1.8.0_412"
OpenJDK Runtime Environment (build 1.8.0_412-b08)
OpenJDK 64-Bit Server VM (build 25.412-b08, mixed mode)

这是编辑环境变量之后的执行结果:

1
2
3
[root@iZ2ze4hnl6pls28qt4w1ttZ matlab]# java -version
Error: dl failure on line 894
Error: failed /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.412.b08-1.el7_9.x86_64/jre/lib/amd64/server/libjvm.so, because /usr/local/MATLAB/MATLAB_Runtime/R2024a/sys/os/glnxa64/libstdc++.so.6: undefined symbol: __cxa_thread_atexit_impl

本来计划重新安装 jdk 的。

在 Linux 上搭建 Java 环境_linux安装java-CSDN博客

1
2
3
4
5
6
7
8
9
[root@iZ2ze4hnl6pls28qt4w1ttZ ~]# yum list | grep jdk
copy-jdk-configs.noarch 3.3-11.el7_9 @updates
java-1.8.0-openjdk.x86_64 1:1.8.0.412.b08-1.el7_9 @updates
java-1.8.0-openjdk-devel.x86_64 1:1.8.0.412.b08-1.el7_9 @updates
java-1.8.0-openjdk-headless.x86_64 1:1.8.0.412.b08-1.el7_9 @updates
java-1.6.0-openjdk.x86_64 1:1.6.0.41-1.13.13.1.el7_3 base
java-1.6.0-openjdk-demo.x86_64 1:1.6.0.41-1.13.13.1.el7_3 base
java-1.6.0-openjdk-devel.x86_64 1:1.6.0.41-1.13.13.1.el7_3 base
java-1.6.0-openjdk-javadoc.x86_64 1:1.6.0.41-1.13.13.1.el7_3 base

结果执行这个命令以后,我明白了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[root@iZ2ze4hnl6pls28qt4w1ttZ ~]# yum list | grep jdk
There was a problem importing one of the Python modules
required to run yum. The error leading to this problem was:

/usr/lib64/python2.7/site-packages/pycurl.so: undefined symbol: CRYPTO_num_locks

Please install a package which provides this module, or
verify that the module is installed correctly.

It's possible that the above module doesn't match the
current version of Python, which is:
2.7.5 (default, Nov 14 2023, 16:14:06)
[GCC 4.8.5 20150623 (Red Hat 4.8.5-44)]

If you cannot solve this problem yourself, please go to
the yum faq at:
http://yum.baseurl.org/wiki/Faq

问题在于 MCR 环境安装以后,影响到了其他类库文件的正常运行。

1
2
3
4
5
[root@iZ2ze4hnl6pls28qt4w1ttZ ~]# gcc --version
gcc (GCC) 4.8.5 20150623 (Red Hat 4.8.5-44)
Copyright © 2015 Free Software Foundation, Inc.
本程序是自由软件;请参看源代码的版权声明。本软件没有任何担保;
包括没有适销性和某一专用目的下的适用性担保。
1
2
3
4
5
6
7
8
9
10
11
12
13
[root@iZ2ze4hnl6pls28qt4w1ttZ ~]# find / -name libstdc++.so.6
/opt/matlab_mcr/sys/os/glnxa64/orig/libstdc++.so.6
/opt/matlab_mcr/sys/os/glnxa64/libstdc++.so.6
/opt/matlab_mcr/bin/glnxa64/libstdc++.so.6
/var/lib/docker/overlay2/8c98e9a3b6ceca81fe97e76c997cc0fd98588bed6e607a6f04a7470506f4e85e/diff/usr/lib64/libstdc++.so.6
/var/lib/docker/overlay2/0d3757448eec0b50754d1ed93e5441da04ad342ce4b2e77784df3ec96c430fa3/diff/usr/lib/libstdc++.so.6
/usr/lib64/libstdc++.so.6
/usr/local/MATLAB/MATLAB_Runtime/R2024a/sys/os/glnxa64/orig/libstdc++.so.6
/usr/local/MATLAB/MATLAB_Runtime/R2024a/sys/os/glnxa64/libstdc++.so.6
/home/matlab/MATLAB_Runtime_R2024a/sys/os/glnxa64/orig/libstdc++.so.6
/home/matlab/MATLAB_Runtime_R2024a/sys/os/glnxa64/libstdc++.so.6
/home/matlab/MATLAB_Runtime_R2024a/bin/glnxa64/libstdc++.so.6
[root@iZ2ze4hnl6pls28qt4w1ttZ ~]#

配置文件:

1
2
3
export LD_LIBRARY_PATH=/usr/local/MATLAB/MATLAB_Runtime/v90/runtime/glnxa64:$LD_LIBRARY_PATH
export LD_LIBRARY_PATH=/usr/local/MATLAB/MATLAB_Runtime/v90/bin/glnxa64:$LD_LIBRARY_PATH
export LD_LIBRARY_PATH=/usr/local/MATLAB/MATLAB_Runtime/v90/sys/os/glnxa64:$LD_LIBRARY_PATH

linux中环境变量配置文件 - 腾讯云开发者社区 - 腾讯云 (tencent.com)

2025 年 2 月 14 日

当然还会报错了:

1
2
export MCR_HOME=/usr/local/MATLAB/MATLAB_Runtime/R2024a
export LD_LIBRARY_PATH=$MCR_HOME/runtime/glnxa64:$MCR_HOME/bin/glnxa64:$MCR_HOME/sys/os/glnxa64:$LD_LIBRARY_PATH
1
2
3
[root@iZ2ze4hnl6pls28qt4w1ttZ R2024a]# java -version
Error: dl failure on line 894
Error: failed /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.412.b08-1.el7_9.x86_64/jre/lib/amd64/server/libjvm.so, because /usr/local/MATLAB/MATLAB_Runtime/R2024a/sys/os/glnxa64/libstdc++.so.6: undefined symbol: __cxa_thread_atexit_impl

问题出在/usr/local/MATLAB/MATLAB_Runtime/R2024a/sys/os/glnxa64/libstdc++.so.6这个库文件上。当你通过设置LD_LIBRARY_PATH来包含MATLAB Runtime的路径时,系统可能会优先加载这个版本的libstdc++.so.6,而不是系统默认的版本。如果MATLAB Runtime中的libstdc++.so.6版本与你的系统上的其他库(如OpenJDK的JVM)不兼容,就可能会出现这样的错误。

尝试删除。

1
2
export MCR_HOME=/usr/local/MATLAB/MATLAB_Runtime/R2024a
export LD_LIBRARY_PATH=$MCR_HOME/runtime/glnxa64:$MCR_HOME/bin/glnxa64:$LD_LIBRARY_PATH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[root@iZ2ze4hnl6pls28qt4w1ttZ matlab]# java -jar _test2.jar 
Exception in thread "main" java.lang.UnsatisfiedLinkError: /usr/local/MATLAB/MATLAB_Runtime/R2024a/bin/glnxa64/libnativedl.so: /lib64/libstdc++.so.6: version `GLIBCXX_3.4.21' not found (required by /usr/local/MATLAB/MATLAB_Runtime/R2024a/bin/glnxa64/libnativedl.so)
at java.lang.ClassLoader$NativeLibrary.load(Native Method)
at java.lang.ClassLoader.loadLibrary0(ClassLoader.java:1934)
at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1817)
at java.lang.Runtime.load0(Runtime.java:782)
at java.lang.System.load(System.java:1100)
at com.mathworks.toolbox.javabuilder.internal.DynamicLibraryUtils.<clinit>(DynamicLibraryUtils.java:47)
at com.mathworks.toolbox.javabuilder.internal.MWMCR.<clinit>(MWMCR.java:1776)
at doubleInput2.DoubleInput2MCRFactory.newInstance(DoubleInput2MCRFactory.java:50)
at doubleInput2.DoubleInput2MCRFactory.newInstance(DoubleInput2MCRFactory.java:66)
at doubleInput2.Class1.<init>(Class1.java:63)
at org.tellhow.matlab.Main.test1(Main.java:23)
at org.tellhow.matlab.Main.main(Main.java:18)
[root@iZ2ze4hnl6pls28qt4w1ttZ matlab]#

Linux中GCC_linux gcc-CSDN博客

1
2
3
4
5
[root@iZ2ze4hnl6pls28qt4w1ttZ matlab]# sudo gcc --version
gcc (GCC) 4.8.5 20150623 (Red Hat 4.8.5-44)
Copyright © 2015 Free Software Foundation, Inc.
本程序是自由软件;请参看源代码的版权声明。本软件没有任何担保;
包括没有适销性和某一专用目的下的适用性担保。
1
2
export MCR_HOME=/usr/local/MATLAB/MATLAB_Runtime/R2024a
export LD_LIBRARY_PATH=$MCR_HOME/runtime/glnxa64:$LD_LIBRARY_PATH

MATLAB Runtime - MATLAB Compiler - MATLAB (mathworks.cn)

更换安装的 Matlab Runtime 版本。

1
2
3
4
5
[root@iZ2ze4hnl6pls28qt4w1ttZ matlab]# uname -r
3.10.0-1160.119.1.el7.x86_64
[root@iZ2ze4hnl6pls28qt4w1ttZ matlab]# cat /etc/redhat-release
CentOS Linux release 7.9.2009 (Core)
[root@iZ2ze4hnl6pls28qt4w1ttZ matlab]#

太特么恶心了吧。

官网上为什么没有版本适配信息。

我把配置文件又改回来了。

1
2
export MCR_HOME=/usr/local/MATLAB/MATLAB_Runtime/R2024a
export LD_LIBRARY_PATH=$MCR_HOME/runtime/glnxa64:$MCR_HOME/bin/glnxa64:$MCR_HOME/sys/os/glnxa64:$LD_LIBRARY_PATH

原来是找不到自己自带的。

那我加回来,不久又回到了七点么。

1
2
3
[root@iZ2ze4hnl6pls28qt4w1ttZ matlab]# java -jar _test2.jar 
Error: dl failure on line 894
Error: failed /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.412.b08-1.el7_9.x86_64/jre/lib/amd64/server/libjvm.so, because /usr/local/MATLAB/MATLAB_Runtime/R2024a/sys/os/glnxa64/libstdc++.so.6: undefined symbol: __cxa_thread_atexit_impl
1
遇到这个新的错误提示,说明在尝试运行Java应用程序时,Java虚拟机(JVM)在加载过程中遇到了问题。具体来说,JVM试图加载其自己的库文件libjvm.so,但在这个过程中,它依赖于libstdc++.so.6库中的一个符号__cxa_thread_atexit_impl,而这个符号在MCR提供的libstdc++.so.6版本中未定义。
1
尝试配置MCR或Java应用程序以使用系统上的libstdc++.so.6库,而不是MCR自带的库。这可能需要调整MCR的配置文件或设置相应的环境变量。

执行以下命令。

1
find / -name libstdc++.so.6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
[root@iZ2ze4hnl6pls28qt4w1ttZ matlab]# find / -name libstdc++.so.6
/opt/matlab_mcr/sys/os/glnxa64/orig/libstdc++.so.6
/opt/matlab_mcr/sys/os/glnxa64/libstdc++.so.6
/opt/matlab_mcr/bin/glnxa64/libstdc++.so.6
/var/lib/docker/overlay2/8c98e9a3b6ceca81fe97e76c997cc0fd98588bed6e607a6f04a7470506f4e85e/diff/usr/lib64/libstdc++.so.6
/var/lib/docker/overlay2/0d3757448eec0b50754d1ed93e5441da04ad342ce4b2e77784df3ec96c430fa3/diff/usr/lib/libstdc++.so.6
/usr/lib64/libstdc++.so.6
/usr/local/MATLAB/MATLAB_Runtime/R2024a/sys/os/glnxa64/orig/libstdc++.so.6
/usr/local/MATLAB/MATLAB_Runtime/R2024a/sys/os/glnxa64/libstdc++.so.6
/home/matlab/MATLAB_Runtime_R2024a/sys/os/glnxa64/orig/libstdc++.so.6
/home/matlab/MATLAB_Runtime_R2024a/sys/os/glnxa64/libstdc++.so.6
/home/matlab/MATLAB_Runtime_R2024a/bin/glnxa64/libstdc++.so.6
find: ‘/proc/13975’: 没有那个文件或目录
find: ‘/proc/13984’: 没有那个文件或目录

再次更改配置,覆盖:

1
2
export MCR_HOME=/usr/local/MATLAB/MATLAB_Runtime/R2024a
export LD_LIBRARY_PATH=/usr/lib64:$MCR_HOME/runtime/glnxa64:$MCR_HOME/bin/glnxa64:$MCR_HOME/sys/os/glnxa64:$LD_LIBRARY_PATH

他妈还报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[root@iZ2ze4hnl6pls28qt4w1ttZ matlab]# java -jar _test2.jar 
Exception in thread "main" java.lang.UnsatisfiedLinkError: /usr/local/MATLAB/MATLAB_Runtime/R2024a/bin/glnxa64/libnativedl.so: /usr/lib64/libstdc++.so.6: version `GLIBCXX_3.4.21' not found (required by /usr/local/MATLAB/MATLAB_Runtime/R2024a/bin/glnxa64/libnativedl.so)
at java.lang.ClassLoader$NativeLibrary.load(Native Method)
at java.lang.ClassLoader.loadLibrary0(ClassLoader.java:1934)
at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1817)
at java.lang.Runtime.load0(Runtime.java:782)
at java.lang.System.load(System.java:1100)
at com.mathworks.toolbox.javabuilder.internal.DynamicLibraryUtils.<clinit>(DynamicLibraryUtils.java:47)
at com.mathworks.toolbox.javabuilder.internal.MWMCR.<clinit>(MWMCR.java:1776)
at doubleInput2.DoubleInput2MCRFactory.newInstance(DoubleInput2MCRFactory.java:50)
at doubleInput2.DoubleInput2MCRFactory.newInstance(DoubleInput2MCRFactory.java:66)
at doubleInput2.Class1.<init>(Class1.java:63)
at org.tellhow.matlab.Main.test1(Main.java:23)
at org.tellhow.matlab.Main.main(Main.java:18)

我特么算是搞明白了。

环境变量这么写,没有问题。

1
2
export MCR_HOME=/usr/local/MATLAB/MATLAB_Runtime/R2024a
export LD_LIBRARY_PATH=$MCR_HOME/runtime/glnxa64:$MCR_HOME/bin/glnxa64:$MCR_HOME/sys/os/glnxa64:$LD_LIBRARY_PATH

但此时系统加载 /usr/local/MATLAB/MATLAB_Runtime/R2024a/sys/os/glnxa64/libstdc++.so.6 时出现了问题,缺个字段。

1
2
rror: dl failure on line 894
Error: failed /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.412.b08-1.el7_9.x86_64/jre/lib/amd64/server/libjvm.so, because /usr/local/MATLAB/MATLAB_Runtime/R2024a/sys/os/glnxa64/libstdc++.so.6: undefined symbol: __cxa_thread_atexit_impl

那我直接改成加载系统自带的变量呗。

1
2
export MCR_HOME=/usr/local/MATLAB/MATLAB_Runtime/R2024a
export LD_LIBRARY_PATH=/usr/lib64:$MCR_HOME/runtime/glnxa64:$MCR_HOME/bin/glnxa64:$MCR_HOME/sys/os/glnxa64:$LD_LIBRARY_PATH

结果加载 /usr/lib64/libstdc++.so.6 又特么出现问题,还是缺个适配版本的 GLIBCXX_3.4.21

1
Exception in thread "main" java.lang.UnsatisfiedLinkError: /usr/local/MATLAB/MATLAB_Runtime/R2024a/bin/glnxa64/libnativedl.so: /usr/lib64/libstdc++.so.6: version `GLIBCXX_3.4.21' not found (required by /usr/local/MATLAB/MATLAB_Runtime/R2024a/bin/glnxa64/libnativedl.so)

总而言之一句话,MATLAB_Runtime R2024a 版本过高,同系统版本 CentOS Linux release 7.9.2009 (Core) 不兼容。

降低版本吧。

不仅仅是 Linux 上重新安装个运行环境这么轻松,也许 Matlab 版本也特么要退回,降低版本。

问题就是我不知道应该降到多少合适,官网上没写。

艹。

2025 年 2 月 17 日

linux下部署Matlab运行环境MCR version2022a - lambertlt - 博客园 (cnblogs.com)

MySQL连接

2025 年 2 月 13 日

matlab 调用MYsql数据库_mob649e81553a70的技术博客_51CTO博客

1
2
3
4
5
6
7
8
9
10
11
12
% 设定数据库的连接地址、数据库名称、用户名和密码
dbname = 'your_database_name'; % 更改为你的数据库名称
username = 'your_username'; % 更改为你的用户名
password = 'your_password'; % 更改为你的密码
jdbc_driver = 'com.mysql.cj.jdbc.Driver'; % MySQL JDBC 驱动程序
url = 'jdbc:mysql://localhost:3306/your_database_name'; % 记得更改为你的数据库地址及端口

% 载入 JDBC 驱动程序
javaaddpath('path_to_your_mysql_connector.jar'); % 记得更改为你的 jar 文件路径

% 创建数据库连接
conn = database(dbname, username, password, jdbc_driver, url);
1
2
3
4
5
6
7
8
% 定义 SQL 查询语句
query = 'SELECT * FROM your_table_name'; % 更改为你的表名

% 执行 SQL 查询
data = fetch(conn, query);

% 显示查询结果
disp(data);
1
2
3
4
5
6
7
8
% 假设数据中包含类别和数量的列
categories = data.category_column; % 更改为你的类别列名
counts = data.count_column; % 更改为你的计数列名

% 创建饼状图
figure;
pie(counts, categories);
title('数据分布饼状图'); % 添加标题
1
2
% 关闭数据库连接
close(conn);

如何下载MySQL数据库的JDBC驱动包? - 酷盾 (kdun.com)

MySQL :: Download MySQL Installer

直接在本地 Maven 仓库下找一个,看能不能用。

MATLAB初学者入门(30)—— 数据库开发_matlab 数据库-CSDN博客

如何使用MATLAB函数访问MySQL数据库? - 酷盾 (kdun.com)

1、安装MySQL Connector/J:首先需要下载并安装MySQL Connector/J驱动程序,这是Java应用程序连接MySQL数据库的官方驱动程序,可以从MySQL官方网站下载最新版本。

2、配置MATLAB环境:将下载的mysql-connector-java-x.x.xx-bin.jar文件复制到MATLAB的java/jar/toolbox目录下,并修改classpath.txt文件,添加以下内容以加载JDBC驱动:

1
$matlabroot/java/jar/toolbox/mysql-connector-java-x.x.xx-bin.jar

3、重启MATLAB:完成上述步骤后,重新启动MATLAB以使更改生效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
% 设定数据库的连接地址、数据库名称、用户名和密码
dbname = 'iois_backend'; % 更改为你的数据库名称
username = 'root'; % 更改为你的用户名
password = 'Dw990831'; % 更改为你的密码
jdbc_driver = 'com.mysql.cj.jdbc.Driver'; % MySQL JDBC 驱动程序
url = 'jdbc:mysql://localhost:3306/iois_backend'; % 记得更改为你的数据库地址及端口

% 载入 JDBC 驱动程序
javaaddpath('E:\Matlab\java\jar\toolbox\mysql\mysql-connector-java-8.0.21.jar'); % 记得更改为你的 jar 文件路径

% 创建数据库连接
conn = database(dbname, username, password, jdbc_driver, url);
disp(conn)

% 检查连接是否成功
if isconnection(conn)
disp('Database connection successful');
else
disp('Database connection failed');
error('Database:ConnectionFailed', 'Failed to connect to database');
end

% 定义 SQL 查询语句
query = 'SELECT * FROM license_info'; % 更改为你的表名

% 执行 SQL 查询
data = fetch(conn, query);

% 显示查询结果
disp(data);

% 假设数据中包含类别和数量的列
categories = data.category_column; % 更改为你的类别列名
counts = data.count_column; % 更改为你的计数列名

% 创建饼状图
figure;
pie(counts, categories);
title('数据分布饼状图'); % 添加标题

% 关闭数据库连接
close(conn);

现在数据库连接失败,安装 Matlab 运行环境也特么失败,这没法进行下去了。

就算数据库连接成功了,还得在 Linux 系统里编译 .m 工程才能生成 Shell 脚本,这就要考虑在 Linux 系统安装编译环境或者直接安装 Matlab。

因为MCR仅仅提供了一个运行环境,并没有提供编译环境,因此还需要在安装了Matlab编译环境的服务器上对.m文件进行编译。

罢了,安装运行环境先不闹了,试试连接数据库吧。

1
2
3
4
5
6
7
8
9
10
11
% 连接到MySQL数据库
conn = database('iois_backend', 'root', 'Dw990831', ...
'com.mysql.jdbc.Driver', 'jdbc:mysql://localhost:3306/iois_backend');

% 查询数据库中的数据
query = 'SELECT * FROM license_info';
data = fetch(conn, query);
% 显示查询结果
disp(data);
% 关闭数据库连接
close(conn);
1
2
3
4
5
6
>> link2
错误使用 database.relational.connection/fetch (第 70 行)
Invalid connection.

出错 link2 (第 7 行)
data = fetch(conn, query);

【MySQL】MySQL中JDBC编程——MySQL驱动包安装——(超详解)_mysql jdbc驱动-CSDN博客

Maven Repository: mysql (mvnrepository.com)

1
2
% 建立数据库连接
conn = database(dbname, username, password, jdbc_driver, url);
1
2
3
>> link3
错误使用 link3 (第 26 行)
数据库连接失败: 输入参数太多。

妈的这样就输入参数太多,那这样呢。

1
2
% 建立数据库连接
conn = database(username, password, jdbc_driver, url);
1
2
3
>> link3
错误使用 link3 (第 26 行)
数据库连接失败: Incorrect number of input arguments. Check documentation for usage

这又显示参数数量不对了。。

Incorrect number of input arguments when plotting in the main script getting data from a nested function - MATLAB Answers - MATLAB Central (mathworks.cn)

看看文档。

MATLAB 快速入门 (mathworks.cn)

database (mathworks.cn)

一上午的努力,功夫不负有心人,总归是连接 MySQL 成功了,官网文档还是有用的。

1
2
3
4
5
6
7
8
9
10
databasename = "iois_backend";
% setSecret("root");
% setSecret("Dw990831");
conn = database(databasename,"root","Dw990831",'Vendor','MySQL', ...
'Server','localhost','PortNumber',3306,'LoginTimeout',5);
disp(conn);

selectquery = "SELECT * FROM license_info";
data = select(conn,selectquery);
disp(data);

这是打印出来的 conn 连接属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
connection - 属性:

DataSource: 'iois_backend'
UserName: 'root'
Driver: 'com.mysql.cj.jdbc.Driver'
URL: 'jdbc:mysql://localhost:33 ...'
Message: ''
Type: 'JDBC Connection Object'
Database Properties:

AutoCommit: 'on'
ReadOnly: 'off'
LoginTimeout: 5
MaxDatabaseConnections: 0

Catalog and Schema Information:

DefaultCatalog: 'iois_backend'
Catalogs: {'apimonitor', 'astar_questionnaire', 'bilibili' ... and 85 more}
Schemas: {}

Database and Driver Information:

DatabaseProductName: 'MySQL'
DatabaseProductVersion: '5.7.19-log'
DriverName: 'MySQL Connector/J'
DriverVersion: 'mysql-connector-java-8.0. ...'

这是测试查询的数据:

image-20250213133220878

那其实就像上午讲的那样,不论是使用 Matlab 工程直接连接数据库并打包成 Shell 脚本在服务器运行,还是编译后打成 jar 包在服务器上用 Java 代码调用工程文件,都需要目标服务器安装 MCR 运行环境,即目标服务器要能够运行 Matlab 工程文件。

Matlab 在一个文件中调用另一个文件中的函数_matlab怎么调用另一个文件函数-CSDN博客

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
%% Call ALL Function
function F = A
F.add = @add;
F.multiply = @multiply;
F.mis = @mis;
end

%% Function body
function c = add(a,b)
c=a+b;
end

function c = multiply(a,b)
c=a*b;
end

function c = mis(a,b)
c=a-b;
1
2
3
4
%% main
A().add(1,2) % 注意,一定要加括号!!!
A().multiply(2,3)
A().mis(4,3)

这样就能调用多个函数了。

Java 调用

使用Java调用Matlab程序代码_java如何调用matlab编写的算法-CSDN博客

在JAVA开发中调用matlab程序_java调用matlab程序-CSDN博客

Matlab代码打包成jar包供java调用_marleb算法 打成jar与java交互怎么使用-CSDN博客

写一段最简单的函数。

1
2
3
function y = doubleInput(x)
y = 2 * x;
end

第一步:确定 Matlab 的 javaJDK 版本

Matlab 在安装的时候一般已经自动安装有 Java 的 JDK,因此我们需要确定 Matlab 软件的JDK 版本号是多少。在 Matlab 命令行窗口输入 version -java,就可以显示 Matlab 软件中的 JDK 版本号。

1
2
3
4
5
>> version -java

ans =

'Java 1.8.0_202-b08 with Oracle Corporation Java HotSpot(TM) 64-Bit Server VM mixed mode'

可以看到我的 Matlab 软件中的JDK版本为1.8.0,因此该 Matlab 软件打包出来的.jar包版本也为 1.8.0。我们只需要电脑上的 Java 环境的版本号前面的大类与其相同就可以,即我这里只需要 1.7 版本的 JDK。(注意:版本号的大类必须相同,否则打包会出现错误!)。

第二步:确定 windows 系统中的 JavaJDK 版本

我们可以通过在 windows 的命令窗口查看自己电脑的 Java JDK 版本号。通过快捷键 win+R,输入cmd,确定后进入命令窗口。

在窗口输入 java -versionjavac -version

1
2
3
4
5
6
7
C:\WINDOWS\system32>java -version
java version "1.8.0_211"
Java(TM) SE Runtime Environment (build 1.8.0_211-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.211-b12, mixed mode)

C:\WINDOWS\system32>javac -version
javac 17.0.9

为什么javac和java的版本不一致_mob64ca12d8c182的技术博客_51CTO博客

可以发现如果显示的 java JDK 版本是1.8版本,注意:这里一定要确认 java 和 javac 的版本是否和 Matlab 中的相同。如果不相同则需要更改环境变量中 Java 的JDK版本,具体怎么修改与 Java 环境变量的配置相同。

第三步:Matlab 代码打包成 .jar 包

如果前面两步都已经完成,那么恭喜你接下来就可以将 Matlab 代码打包成 .jar 包了。这里建议关闭 Matlab 软件重新启动一遍。因为很有可能你在第二步中更改了 windows 的 JDK 版本,那么就需要重启 Matlab 软件,否则 Matlab 软件还是不能成功打包.jar包。原因就是Matlab 软件还没反应过来系统的 JDK 已经发生了更改。

首先在 Matlab 命令窗口输入 deploytool 会立刻弹出 Compiler 窗口如下:

image-20250212110634049

选择其中的 Library Compiler 点击,就进入了代码打包窗口:

image-20250212111002835

打包成功,同时打包好的.jar包所在的文件夹也会弹出。

Oracle open JDK和 Amazon Corretto JDK的区别_corretto和jdk的区别-CSDN博客

IDEA引入本地jar包的几种方法 - 青喺半掩眉砂 - 博客园 (cnblogs.com)

IDEA手动导入第三方Jar包_idea引入外部jar包-CSDN博客

1
2
3
4
5
6
7
8
DoubleInput doubleInput = null;
try {
doubleInput = new DoubleInput();
Object[] objects = doubleInput.doubleInput(6);
System.out.println(objects[0]);
} catch (MWException e) {
throw new RuntimeException(e);
}

matlab调用java程序 matlab调用java界面_mob6454cc6e8f43的技术博客_51CTO博客

1
C:\Program Files\Java\jdk1.8.0_211\bin

matlab如何设置java环境变量地址_百度知道 (baidu.com)

更改 matlab java 版本,在Matlab中更改默认的JVM版本-CSDN博客

1
setenv('JAVA_HOME','C:\Program Files\Java\jdk1.8.0_211')

特么打包总算不用报错了吧,环境变量配置好了。

1
2
3
4
java: 无法访问doubleInput.DoubleInput
错误的类文件: /D:/Project/tellhow/广西项目/test/doubleInput/for_redistribution_files_only/doubleInput.jar!/doubleInput/DoubleInput.class
类文件具有错误的版本 61.0, 应为 52.0
请删除该文件或确保该文件位于正确的类路径子目录中。

本机 Java 环境变量以及 Matlab Java 环境变量不一致,本机使用 1.8_211 编译,Matlab 使用 1.8_362 编译打包,小版本不一致也会调用出错。

配置好本机 Java 环境变量以及 Matlab Java 环境变量后,这次调用总算没有出现那样的错误了。

1
2
3
4
5
6
7
try {
Class1 class1 = new Class1();
Object[] objects = class1.doubleInput(6);
System.out.println(objects[0]);
} catch (MWException e) {
throw new RuntimeException(e);
}

新的报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Hello world!
错误使用 doubleInput
输出参数太多。

Exception in thread "main" java.lang.RuntimeException: com.mathworks.toolbox.javabuilder.MWException: Êä³ö²ÎÊý
at org.tellhow.matlab.Main.main(Main.java:25)
Caused by: com.mathworks.toolbox.javabuilder.MWException: Êä³ö²ÎÊý
at com.mathworks.toolbox.javabuilder.internal.MWMCR.mclFeval(Native Method)
at com.mathworks.toolbox.javabuilder.internal.MWMCR.access$600(MWMCR.java:33)
at com.mathworks.toolbox.javabuilder.internal.MWMCR$6.mclFeval(MWMCR.java:898)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.mathworks.toolbox.javabuilder.internal.MWMCR$2.invoke(MWMCR.java:785)
at com.sun.proxy.$Proxy0.mclFeval(Unknown Source)
at com.mathworks.toolbox.javabuilder.internal.MWMCR.invoke(MWMCR.java:448)
at doubleInput2.Class1.doubleInput(Class1.java:185)
at org.tellhow.matlab.Main.main(Main.java:22)

点进去看方法,是这样的。

1
2
3
4
5
public Object[] doubleInput(int var1, Object... var2) throws MWException {
Object[] var3 = new Object[var1];
this.fMCR.invoke(Arrays.asList(var3), MWMCR.getRhsCompat(var2, sDoubleInputSignature), sDoubleInputSignature);
return var3;
}

第一个参数是指定传参数量,从第二个参数开始才是真正应该传递的参数。

哎哟卧槽,调用成功了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void test1(){
try {
Class1 class1 = new Class1();
// 调用 doubleInput 方法,只传递必要的输入参数(在这个例子中是 6)
// 注意:这里的 var2 应该是空的,因为我们没有额外的输入参数
Object[] result = class1.doubleInput(1, 6);
// 检查结果数组是否不为空且至少有一个元素
if (result != null && result.length > 0) {
// 将结果转换为适当的类型(在这个例子中,它应该是一个 Double)
System.out.println(result[0]);
} else {
System.out.println("没有返回结果");
}
} catch (MWException e) {
e.printStackTrace();
throw new RuntimeException("MATLAB 函数调用失败", e);
}
}

接下来考虑下,这个过程中有安装 MCR 么,没有 Matlab 编译运行时环境还能不能跑这段代码?

特么当然用到了。

1
"D:\Project\tellhow\广西项目\test\doubleInput2\for_redistribution\MyAppInstaller_web.exe"

这是打包好 doubleInput 项目以后,在这个目录下找到了运行环境安装可执行文件,同我上面解压出来的运行环境安装包是一样的。

最终都在本机配置了相应的环境变量。

image-20250212153238183

不过很明显,后者要占用磁盘空间更大一些,估计这是未压缩版。

image-20250212153451726

那么如果是在 Linux 系统下,前者 .exe 可执行文件也许就不适用了,只能采用解压运行环境压缩包来配置环境,还需要手动配置环境变量。

不好意思看走眼了,两个都是 .exe 文件格式。

那么现在本机已经成功实现使用 Java 调用 matlab 打包的 jar 包程序,调用数据分析接口。

我可以试一试,先测试运行环境变量安装,成功后再研究调用本身。

手动构建工程打成 jar 包,运行成功。

image-20250212151143774

这就是 jar 包路径。

1
D:\Project\tellhow\matlab\out\artifacts\_test2_jar
1
2
D:\Project\tellhow\matlab\out\artifacts\_test2_jar>java -jar _test2.jar
12

上传 jar 包至 Linux 测试环境中,运行,果然报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
(base) [root@thmn matlab]# java -jar _test2.jar 
Exception in thread "main" java.lang.UnsatisfiedLinkError: Failed to find the required library libmwmclmcrrt.so.24.1 on java.library.path.
This library is typically installed along with MATLAB or the MATLAB Runtime. Its absence may indicate an issue with that installation or
the current path configuration, or a mismatch with the architecture of the Java interpreter on the path.
MATLAB Runtime version this component is attempting to use: 24.1.
Java interpreter architecture: glnxa64.

at com.mathworks.toolbox.javabuilder.internal.MCRConfiguration$ProxyLibraryDir.get(MCRConfiguration.java:195)
at com.mathworks.toolbox.javabuilder.internal.MCRConfiguration$ProxyLibraryDir.<clinit>(MCRConfiguration.java:205)
at com.mathworks.toolbox.javabuilder.internal.MCRConfiguration.getProxyLibraryDir(MCRConfiguration.java:210)
at com.mathworks.toolbox.javabuilder.internal.MCRConfiguration$MCRRoot.get(MCRConfiguration.java:64)
at com.mathworks.toolbox.javabuilder.internal.MCRConfiguration$MCRRoot.<clinit>(MCRConfiguration.java:76)
at com.mathworks.toolbox.javabuilder.internal.MCRConfiguration.getMCRRoot(MCRConfiguration.java:81)
at com.mathworks.toolbox.javabuilder.internal.MCRConfiguration$ModuleDir.<clinit>(MCRConfiguration.java:53)
at com.mathworks.toolbox.javabuilder.internal.MCRConfiguration.getModuleDir(MCRConfiguration.java:58)
at com.mathworks.toolbox.javabuilder.internal.MWMCR.<clinit>(MWMCR.java:1775)
at doubleInput2.DoubleInput2MCRFactory.newInstance(DoubleInput2MCRFactory.java:50)
at doubleInput2.DoubleInput2MCRFactory.newInstance(DoubleInput2MCRFactory.java:66)
at doubleInput2.Class1.<init>(Class1.java:63)
at org.tellhow.matlab.Main.test1(Main.java:23)
at org.tellhow.matlab.Main.main(Main.java:18)
(base) [root@thmn matlab]#

不过为什么我的虚拟机上传文件失败了呢,这又是个问题。

2025 年 2 月 14 日

今天测试下 Matlab 各个接口,将来连接到数据库才能拿到测试参数。

官方文档:MATLAB 快速入门 (mathworks.cn)

针对这个函数,写个测试代码吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Mbg = getMbgMatrix(gen,bus)
%%输入gen,bus参数
ng = size(gen,1);
nb = size(bus,1);

Mbg = zeros(nb,ng);

for ii = 1:nb
for jj = 1:ng
if ii == gen(jj,1)
Mbg(ii,jj) = 1;
end
end
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
function testGetMbgMatrix()
addpath("../../虚拟电厂调控/")
% 定义示例发电机数据
% 假设第一列是发电机连接的母线编号
gen = [
1, 100, 'A'; % 发电机1连接在母线1上,功率100,标识符'A'(此处'A'仅为示例,实际函数未使用)
2, 150, 'B'; % 发电机2连接在母线2上,功率150,标识符'B'
1, 200, 'C'; % 发电机3连接在母线1上,功率200,标识符'C'
3, 120, 'D' % 发电机4连接在母线3上,功率120,标识符'D'
];
% 注意:实际测试中,我们只需要发电机的母线编号,因此可以只取gen的第一列。
% 但为了模拟真实数据,这里保留了其他列。

% 定义示例母线数据
% 这里我们只需要母线的数量,因此可以简单地定义一个行数足够的矩阵。
% 实际上,母线数据可能包含电压等级、类型等信息,但本函数不需要这些。
bus = [
1; % 母线1
2; % 母线2
3 % 母线3
];

% 由于getMbgMatrix函数只使用gen的第一列和bus的行数,我们提取这些信息。
gen_bus_numbers = gen(:, 1); % 提取发电机连接的母线编号

% 调用待测试函数
Mbg = getMbgMatrix(gen_bus_numbers, bus);

% 打印结果
disp('Mbg Matrix:');
disp(Mbg);

% 检查结果是否符合预期(手动或通过自动化测试)
% 预期结果:
% 更新后的预期结果(基于实际返回的 Mbg 大小)
expected_Mbg = [
1, 0, 1, 0; % 假设这是基于您的测试数据和函数实现得出的正确结果
0, 1, 0, 0;
0, 0, 0, 1 % 如果最后一列表示另一个连接到某个未明确在bus中定义的母线的发电机,
% 那么这里应该根据实际情况来调整。如果这不是预期的行为,那么可能需要检查函数实现。
];
% 注意:上面的expected_Mbg是基于我们的测试数据和假设得出的,实际使用时需要根据具体情况调整。
% 在这个例子中,由于我们没有考虑发电机4(因为它连接到未在bus中定义的母线3',这里3'仅用于说明,实际中应理解为另一个未在bus列表中的母线编号),
assert(isequal(Mbg, expected_Mbg), 'The Mbg matrix does not match the expected result.');
end

嗨哟,妈的,测试个 数据库连接竟然还得再次指定下 JDBC 驱动安装位置。

1
javaaddpath("E:\Matlab\java\jar\toolbox\mysql\mysql-connector-java-8.0.28.jar")
1
2
3
4
5
6
7
8
9
10
11
12
function databaseConnector()
databasename = "iois_backend";
% setSecret("root");
% setSecret("Dw990831");
conn = database(databasename,"root","Dw990831",'Vendor','MySQL', ...
'Server','localhost','PortNumber',3306,'LoginTimeout',5);

disp(conn);

selectquery = "SELECT * FROM license_info";
data = select(conn,selectquery);
disp(data);

连接数据库,执行 SQL 查询。

1
2
3
4
5
6
7
8
9
function conn = databaseConnector()
databasename = "iois_backend";
% setSecret("root");
% setSecret("Dw990831");
% 连接 MySQL 数据库
conn = database(databasename,"root","Dw990831",'Vendor','MySQL', ...
'Server','localhost','PortNumber',3306,'LoginTimeout',5);

disp(conn);
1
2
3
4
5
6
7
8
9
function selectLicenseInfo()
% 获取数据库连接
conn = databaseConnector();
% 编写 SQL 语句
selectquery = "SELECT * FROM license_info";
% 执行查询
data = select(conn,selectquery);
% 打印
disp(data);

尝试读取配置文件。

1
2
3
4
5
6
7
8
9
{
"databaseName": "iois_backend",
"username": "root",
"password": "Dw990831",
"vendor": "MySQL",
"server": "localhost",
"portNumber": 3306,
"loginTimeout": 5
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function conn = databaseConnectorFromJson()
% 读取配置文件
configFilePath = './dbConfig.json'; % 替换为你的JSON文件路径
configData = jsondecode(fileread(configFilePath));

% 从配置文件中提取数据库连接信息
databaseName = configData.databaseName;
username = configData.username;
password = configData.password;
vendor = configData.vendor;
server = configData.server;
portNumber = configData.portNumber;
loginTimeout = configData.loginTimeout;

% 连接数据库
conn = database(databaseName, username, password, 'Vendor', vendor, ...
'Server', server, 'PortNumber', portNumber, 'LoginTimeout', loginTimeout);
end

2025 年 2 月 17 日

导入 Excel 表格数据。

1
2
3
4
5
6
7
8
9
10
%分布式发电历史数据
P_DG_history=xlsread('DG-test.xlsx');

for i=1:N_DG
L_DG(1,i)=24;
end

P_DG_max=max(max(P_DG_history(1,:)));

toc;
1
P_DG_order=zeros(N_DG,T);%存放概率之和最大时对应的排列组合

Linux系统中安装MATLAB和Mathematica-不念博客 (bunian.cn)

清理下电脑,只好是删除掉已经安装过的虚拟机,反正各种问题的解决方案都已经记录清楚了。

重新安装麒麟V10。

麒麟V10

2025 年 2 月 14 日

安装银河麒麟桌面系统V10【超详细图文教程】 - 知乎 (zhihu.com)

【国产化信创平台】麒麟银河V10 Linux系统安装流程_51CTO博客_银河麒麟 v10 安装

手把手教你安装银河麒麟V10操作系统 (baidu.com)

银河麒麟服务器操作系统V10安装步骤 (baidu.com)

银河麒麟V10桌面操作系统安装教程_银河麒麟操作系统v10-CSDN博客

国产操作系统、麒麟操作系统——麒麟软件官方网站 (kylinos.cn)

下载光盘映像文件成功,安装。

新建虚拟机

  • 打开VMware Workstation软件,点击左上方文件,在弹出菜单中选择新建虚拟机;

  • 选择典型,点击下一步;

  • 选择稍后安装操作系统,点击下一步;

  • 选择Linux,版本选择其他Liunx 5.x 及更高版本内核 64 位,点击下一步;

  • 填写虚拟机名称,选择部署位置,点击下一步;

  • 指定磁盘大小(50GB以上,后续一键安装对磁盘容量有需求),选择将虚拟磁盘拆分成多个文件,点击下一步;

  • 点击完成;

  • 至此,已完成虚拟机创建。

麒麟系统安装

  • 启动虚拟机后,进入银河麒麟系统安装界面,改界面可选择使用银河麒麟操作系统而不安装,直接进入试用系统,试用系统中同样可以安装麒麟系统;也可通过键盘上下键,选择第二项安装银河麒麟操作系统,直接进行系统安装;
  • 进入试用系统后,可双击安装Kylin,安装麒麟系统;
  • 选择语言(简体中文),同意条款,选择时区(北京),选择从Live安装;
  • 勾选虚拟硬盘,点击下一步,勾选格式化整个磁盘,点击下一步;
  • 创建账户,点击下一步,填写账户信息,点击下一步;
  • 选择要安装的应用,点击开始安装;
  • 等待系统安装完成即可。

本机虚拟机卡死了,特么根本安装不了。

要么还是下周直接在 120 机器上部署吧,特么的。

麒麟v10sp3操作系统安装保姆级教程(一)_麒麟10操作系统-CSDN博客

麒麟v10sp3操作系统安装保姆级教程(二)_麒麟sp3-CSDN博客

虚拟机安装有点卡顿。

听会儿歌让自己清醒清醒。

主流Linux发行版本区别(CentOS、麒麟、Ubuntu) - 老虎死了还有狼 - 博客园 (cnblogs.com)

国产系统崛起:银河麒麟系统2403新手安装教程 (baidu.com)

原来自定义分区是这么操作的。

image-20250214173059224

1
memory	memory-pc	Dw990831@

image-20250214173334517

2025 年 2 月 17 日

使用扫码方式激活银河麒麟_银河麒麟激活-CSDN博客

银河麒麟操作系统安装matlab时遇到的问题_麒麟系统安装matlab-CSDN博客

linux下安装MATLAB (各版本通用)_matlab linux-CSDN博客

直接卸载掉旧虚拟机,安装新的虚拟机,磁盘空间给到五十个GB,我的电脑 D 盘算是马上给撑满了。

MCR_R2018b 下载完毕,转发至麒麟桌面,准备解压安装。

同样的步骤。

1
sudo ./install -mode silent -agreeToLicense  yes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
输入密码        
Preparing installation files ...
Installing ...
(二月 17, 2025 14:23:48) ##################################################################
(二月 17, 2025 14:23:48) #
(二月 17, 2025 14:23:48) # Today's Date:
(二月 17, 2025 14:23:48) Mon Feb 17 14:23:48 CST 2025
(二月 17, 2025 14:23:48)
(二月 17, 2025 14:23:48) System Info
(二月 17, 2025 14:23:48) OS: Linux 5.10.0-9-generic
(二月 17, 2025 14:23:48) Arch: amd64
(二月 17, 2025 14:23:48) Data Model: 64
(二月 17, 2025 14:23:48) Language: zh
(二月 17, 2025 14:23:48) Java Vendor: Oracle Corporation
(二月 17, 2025 14:23:48) Java Home: /tmp/mathworks_10506/sys/java/jre/glnxa64/jre
(二月 17, 2025 14:23:48) Java Version: 1.8.0_152
(二月 17, 2025 14:23:48) Java VM Name: Java HotSpot(TM) 64-Bit Server VM
(二月 17, 2025 14:23:48) Java Class Path: /tmp/mathworks_10506/java/config/installagent/pathlist.jar
(二月 17, 2025 14:23:48) User Name: root
(二月 17, 2025 14:23:48) Current Directory: /tmp/mathworks_10506
(二月 17, 2025 14:23:48) Input arguments:
(二月 17, 2025 14:23:48) root /home/memory/mcr/MCR_R2018b_glnxa64_installer
(二月 17, 2025 14:23:48) libdir /tmp/mathworks_10506
(二月 17, 2025 14:23:48) mode silent
(二月 17, 2025 14:23:48) agreeToLicense yes
(二月 17, 2025 14:23:48) standalone true
(二月 17, 2025 14:23:48) connectionMode OFFLINE_ONLY
(二月 17, 2025 14:23:50) Starting local product/component search in download directory
(二月 17, 2025 14:23:50) Searching for archives...
(二月 17, 2025 14:23:50) Reading /home/memory/mcr/MCR_R2018b_glnxa64_installer/archives
(二月 17, 2025 14:23:50) 正在汇集产品列表...
(二月 17, 2025 14:23:50) 2234 files found in /home/memory/mcr/MCR_R2018b_glnxa64_installer/archives
(二月 17, 2025 14:23:50) Reading /home/memory/mcr/MCR_R2018b_glnxa64_installer
(二月 17, 2025 14:23:50) 9 files found in /home/memory/mcr/MCR_R2018b_glnxa64_installer
(二月 17, 2025 14:23:50) Archive search complete. 2243 total files found.
(二月 17, 2025 14:23:53) Completed local product/component search
(二月 17, 2025 14:23:53) Starting local product/component search in download directory
(二月 17, 2025 14:23:53) Searching for archives...
(二月 17, 2025 14:23:53) /usr/local/MATLAB/MATLAB_Runtime/v95/archives doesn't exist ... skipping.
(二月 17, 2025 14:23:53) Archive search complete. 0 total files found.
(二月 17, 2025 14:23:53) Completed local product/component search
(二月 17, 2025 14:23:53) Installing Product: MATLAB Runtime - Core 9.5
(二月 17, 2025 14:24:55) Installing Product: MATLAB Runtime - GPU 9.5
(二月 17, 2025 14:25:27) Installing Product: MATLAB Runtime - Hadoop And Spark 9.5
(二月 17, 2025 14:25:27) Installing Product: MATLAB Runtime - Java 9.5
(二月 17, 2025 14:25:27) Installing Product: MATLAB Runtime - MPS 9.5
(二月 17, 2025 14:25:28) Installing Product: MATLAB Runtime - NET And XL 9.5
(二月 17, 2025 14:25:28) Installing Product: MATLAB Runtime - Numerics 9.5
(二月 17, 2025 14:25:42) Installing Product: MATLAB Runtime - Web Apps 9.5
(二月 17, 2025 14:25:45) java.io.FileNotFoundException: /usr/local/MATLAB/MATLAB_Runtime/v95/appdata/installedProductData.txt (权限不够)
at java.io.FileOutputStream.open0(Native Method)
at java.io.FileOutputStream.open(FileOutputStream.java:270)
at java.io.FileOutputStream.(FileOutputStream.java:213)
at com.mathworks.instutil.FileIO.copyToFileFromStream(FileIO.java:509)
at com.mathworks.instutil.FileIO.createFileFromStream(FileIO.java:168)
at com.mathworks.instutil.FileIO.writeStringToFile(FileIO.java:351)
at com.mathworks.install_impl.InstalledProductDataImpl.writeInstalledProductData(InstalledProductDataImpl.java:260)
at com.mathworks.install_impl.InstallerImpl.install(InstallerImpl.java:142)
at com.mathworks.installwizard.model.InstallTask.execute(InstallTask.java:46)
at com.mathworks.installwizard.model.AbstractBackgroundTask.execute(AbstractBackgroundTask.java:38)
at com.mathworks.install_task.AbstractInstallTask.call(AbstractInstallTask.java:50)
at com.mathworks.install_task.AbstractInstallTask.call(AbstractInstallTask.java:18)
at com.mathworks.wizard.worker.WorkerImpl.doInBackground(WorkerImpl.java:24)
at javax.swing.SwingWorker$1.call(SwingWorker.java:295)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at javax.swing.SwingWorker.run(SwingWorker.java:334)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)

(二月 17, 2025 14:25:45) Error: /usr/local/MATLAB/MATLAB_Runtime/v95/appdata/installedProductData.txt (权限不够)
(二月 17, 2025 14:25:45) Exiting with status -1
(二月 17, 2025 14:25:45) End - Unsuccessful.
Finished

配置环境变量。

1
sudo vim /etc/profile
1
export LD_LIBRARY_PATH=/usr/local/MATLAB/MATLAB_Runtime/v98/runtime/glnxa64:/usr/local/MATLAB/MATLAB_Runtime/v98/bin/glnxa64:/usr/local/MATLAB/MATLAB_Runtime/v98/extern/bin/glnxa64:$LD_LIBRARY_PATH
1
source /etc/profile
1
2
3
4
memory@memory-pc:/usr/local/MATLAB/MATLAB_Runtime/v95$ echo $LD_LIBRARY_PATH
/usr/local/MATLAB/MATLAB_Runtime/v95/runtime/glnxa64:/usr/local/MATLAB/MATLAB_Runtime/v95/bin/glnxa64:/usr/local/MATLAB/MATLAB_Runtime/v95/extern/bin/glnxa64:
memory@memory-pc:/usr/local/MATLAB/MATLAB_Runtime/v95$
memory@memory-pc:/usr/local/MATLAB/MATLAB_Runtime/v95$

看起来麒麟V10安装 MCR 很顺利,已经成功了。

linux忘记sudo密码 - 腾讯云开发者社区 - 腾讯云 (tencent.com)

更新当前用户密码:

1
2
3
4
5
memory@memory-pc:~/桌面$ sudo passwd
输入密码
新的密码:
重新输入新的密码:
passwd:已成功更新密码

更新 root 用户密码:

1
2
3
4
memory@memory-pc:~/桌面$ sudo passwd root
新的密码:
重新输入新的密码:
passwd:已成功更新密码

怪了,虚拟机 ping 外部资源可以 ping 通,但本机 ping 虚拟机 就会失败,无法访问目标主机。

1
C:\WINDOWS\system32>ping 192.168.229.130                                                                                                                                                                                                        正在 Ping 192.168.229.130 具有 32 字节的数据:                                                                           来自 192.168.229.130 的回复: 无法访问目标主机。                                                                         来自 192.168.229.130 的回复: 无法访问目标主机。                                                                         来自 192.168.229.130 的回复: 无法访问目标主机。                                                                         来自 192.168.229.130 的回复: 无法访问目标主机。                                                                                                                                                                                                 192.168.229.130 的 Ping 统计信息:                                                                                           数据包: 已发送 = 4,已接收 = 4,丢失 = 0 (0% 丢失),                                   
1
2
3
4
5
6
root@memory-pc:~# ping baidu.com
PING baidu.com (39.156.66.10) 56(84) bytes of data.
64 bytes from 39.156.66.10 (39.156.66.10): icmp_seq=1 ttl=128 time=28.7 ms
64 bytes from 39.156.66.10 (39.156.66.10): icmp_seq=2 ttl=128 time=27.3 ms
64 bytes from 39.156.66.10 (39.156.66.10): icmp_seq=3 ttl=128 time=34.3 ms
64 bytes from 39.156.66.10 (39.156.66.10): icmp_seq=4 ttl=128 time=30.6 ms

试下这条命令。

1
sudo iptables -L

银河麒麟V10系统默认不能ping通的解决方案-加固计算机,加固笔记本,军用计算机,加固平板电脑,三防电脑,加固笔记本,加固服务器,三防笔记本-四川长风致远科技有限公司-官方网站 (chinfort.com)

1
sudo iptables -F

国产操作系统银河麒麟V10,遇到了不能ping通的问题,能上网,也能ping通别的电脑,大概率是防火墙的问题,通过修改防火墙设置可以正常使用ssh。

  1. 右键桌面空白处打开终端,输入 sudo iptables-F 后回车,此命令用于清空关闭防火墙。提示输入系统密码,完成后再次回车,此时已经可以通过局域网内的其他电脑ping通麒麟主机,且XSHELL等也可以正常使用。
  2. 为了重启后能够让防火墙状态继续保持关闭,还需做如下修改。继续在终端中输入 sudo pluma /etc/rc.local 后回车,然后在如下界面的第13行输入sudo iptables -F,然后保存退出,此命令用于把关闭防火墙命令加入到系统启动项中。
1
sudo pluma /etc/rc.local

至此麒麟主机每次重启或关闭,防火墙依然会持续保持关闭状态,其他主机即可正常访问。

哎哟卧槽,还真的管用,主机直接 Ping 通了。

1
2
3
4
5
6
7
8
9
10
11
12
C:\WINDOWS\system32>ping 192.168.229.130

正在 Ping 192.168.229.130 具有 32 字节的数据:
来自 192.168.229.130 的回复: 字节=32 时间<1ms TTL=64
来自 192.168.229.130 的回复: 字节=32 时间<1ms TTL=64
来自 192.168.229.130 的回复: 字节=32 时间<1ms TTL=64
来自 192.168.229.130 的回复: 字节=32 时间<1ms TTL=64

192.168.229.130 的 Ping 统计信息:
数据包: 已发送 = 4,已接收 = 4,丢失 = 0 (0% 丢失),
往返行程的估计时间(以毫秒为单位):
最短 = 0ms,最长 = 0ms,平均 = 0ms

不过真见鬼了,本机可以 Ping 通,但 FinalShell 远程连接始终失败。

1
192.168.118.120      th    th123456

Matlab 安装

2025 年 2 月 17 日

linux下安装MATLAB (各版本通用)_matlab linux-CSDN博客

Linux 安装 Matlab。

下载安装包,即将下载完成。

解压。

1
unzip MathWorks.MATLAB.R2018blinux.zip -d /home/memory/matlab/Matlab_R2018b
1
2
3
4
5
inflating: /home/memory/matlab/Matlab_R2018b/R2018b/version.txt  
inflating: /home/memory/matlab/Matlab_R2018b/R2018b/VersionInfo.xml
inflating: /home/memory/matlab/Matlab_R2018b/公众号免责声明.txt
inflating: /home/memory/matlab/Matlab_R2018b/公众号网站:羽享平台.url
inflating: /home/memory/matlab/Matlab_R2018b/更多资源请关注Linux资源库公众号.png

解压成功。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(base) [root@thmn Matlab_R2018b]# cd R2018b/
(base) [root@thmn R2018b]# ll
总用量 3764
-rw-r--r-- 1 root root 3371 3月 21 2011 activate.ini
drwxr-xr-x 7 root root 4096 8月 29 2018 archives
drwxr-xr-x 3 root root 4096 8月 29 2018 bin
drwxr-xr-x 3 root root 4096 8月 29 2018 etc
drwxr-xr-x 5 root root 4096 8月 29 2018 help
-rw-r--r-- 1 root root 8179 5月 25 2018 install
-rw-r--r-- 1 root root 10118 7月 28 2018 installer_input.txt
-rw-r--r-- 1 root root 3684132 8月 22 2018 install_guide.pdf
drwxr-xr-x 5 root root 4096 8月 29 2018 java
-rw-r--r-- 1 root root 78909 7月 28 2018 license_agreement.txt
-rw-r--r-- 1 root root 12035 8月 7 2018 patents.txt
-rw-r--r-- 1 root root 6641 7月 28 2018 readme.txt
drwxr-xr-x 4 root root 4096 8月 29 2018 sys
-rw-r--r-- 1 root root 245 12月 28 2013 trademarks.txt
drwxr-xr-x 3 root root 4096 8月 29 2018 ui
-rw-r--r-- 1 root root 301 8月 29 2018 VersionInfo.xml
-rw-r--r-- 1 root root 32 8月 29 2018 version.txt

执行安装。

1
./install
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
(base) [root@thmn R2018b]# ./install
-bash: ./install: 权限不够
(base) [root@thmn R2018b]# sudo chmod 777 install
(base) [root@thmn R2018b]# ./install
./install:行269: /home/memory/matlab/Matlab_R2018b/R2018b/bin/glnxa64/install_unix: 权限不够
./install: 第 269 行:exec: /home/memory/matlab/Matlab_R2018b/R2018b/bin/glnxa64/install_unix: 无法执行: 权限不够
(base) [root@thmn R2018b]# cd /home/memory/matlab/Matlab_R2018b/R2018b/bin/glnxa64/install_unix
-bash: cd: /home/memory/matlab/Matlab_R2018b/R2018b/bin/glnxa64/install_unix: 不是目录
(base) [root@thmn R2018b]# cd /home/memory/matlab/Matlab_R2018b/R2018b/bin/glnxa64/
(base) [root@thmn glnxa64]# ll
总用量 11416
-rw-r--r-- 1 root root 30048 5月 25 2018 install_unix
-rw-r--r-- 1 root root 2712114 5月 25 2018 libcrypto.so.1
-rw-r--r-- 1 root root 2712114 5月 25 2018 libcrypto.so.1.0.0
-rw-r--r-- 1 root root 92664 5月 25 2018 libgcc_s.so.1
-rw-r--r-- 1 root root 1286856 7月 18 2018 libinstutil.so
-rw-r--r-- 1 root root 194709 5月 25 2018 libmwcpp11compat.so
-rw-r--r-- 1 root root 177368 7月 11 2018 libmwflcertificates.so
-rw-r--r-- 1 root root 23128 7月 11 2018 libmwinstall.so
-rw-r--r-- 1 root root 181344 7月 11 2018 libmwinstlic_4a.so
-rw-r--r-- 1 root root 51912 7月 11 2018 libmwwebproxy.so
-rw-r--r-- 1 root root 10616 7月 11 2018 libnativenet.so
-rw-r--r-- 1 root root 10584 7月 11 2018 libnativewebproxy.so
-rw-r--r-- 1 root root 520480 5月 25 2018 libssl.so.1
-rw-r--r-- 1 root root 520480 5月 25 2018 libssl.so.1.0.0
-rw-r--r-- 1 root root 1561792 5月 25 2018 libstdc++.so.6
-rw-r--r-- 1 root root 1561792 5月 25 2018 libstdc++.so.6.0.22
(base) [root@thmn glnxa64]# sudo chmod 777 install_unix
1
2
3
4
(base) [root@thmn R2018b]# ./install
Preparing installation files ...
Installing ...
Finished
1
chmod +x /home/memory/matlab/Matlab_R2018b/R2018b/sys/java/jre/glnxa64/jre/bin/java

还有问题。

1
2
3
4
5
6
7
8
9
(base) [root@thmn R2018b]# ./install
Preparing installation files ...
Installing ...
---------------------------------------------------------------------------
错误: 安装无法继续。您可能需要执行以下任一操作:
1.设置 X11 显示,然后重新启动安装过程
2.通过指定 -mode silent 选项使用静默安装功能
---------------------------------------------------------------------------
Finished

直接执行,静默安装。

1
./install -mode silent

安装失败。

1
2
3
4
(二月 17, 2025 16:46:33) When running the installer with an input file, you must accept the license agreement by setting the agreeToLicense option to yes.
(二月 17, 2025 16:46:33) Exiting with status -2
(二月 17, 2025 16:46:35) End - Unsuccessful.
Finished
1
./install -mode silent -agreeToLicense  yes

还是安装失败。

1
2
3
4
(二月 17, 2025 16:46:51) When running the installer with an input file, you must provide a File Installation Key using the fileInstallationKey option.
(二月 17, 2025 16:46:51) Exiting with status -2
(二月 17, 2025 16:46:53) End - Unsuccessful.
Finished
1
./install -mode silent -agreeToLicense  yes -fileInstallationKey 09806-07443-53955-64350-21751-41297

失策了,没有安装页面弹窗。

1
2
3
4
5
6
7
(二月 17, 2025 16:50:16) 指定的位置中可能没有足够的空间。是否仍要继续?

要继续,请点击“是”。要返回并选择其他文件夹,请点击“否”。
(二月 17, 2025 16:50:16) 指定的位置中可能没有足够的空间。是否仍要继续?

要继续,请点击“是”。要返回并选择其他文件夹,请点击“否”。
(二月 17, 2025 16:50:16) Installing Product: MATLAB Distributed Computing Server 6.13

现在有两个问题:FinalShell 安装 Matlab 失败(空间不足,没有窗口交互页面);VMWare 连接远程服务器失败。

没有窗口交互,那么有两个解决方案:尝试 VMWare 连接远程服务器;安装其他 SSH 远程连接工具软件。

或者直接解决空间不足问题,尝试 Finall Shell 一键安装。

怎么查看linux目录大小 - 腾讯云开发者社区 - 腾讯云 (tencent.com)

查看当前目录占用磁盘空间大小。

1
du -sh

查看当前目录剩余磁盘空间大小。

1
df -h .

指定安装目录。

1
./install -mode silent -agreeToLicense yes -fileInstallationKey 09806-07443-53955-64350-21751-41297 -destinationFolder /home/memory/matlab/Matlab_R2018b/dev

开始安装了。

这么快就又折腾到晚五点多了,还有十来分钟就可以下班回家咯。

安装应该是结束了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
(二月 17, 2025 17:25:00) Notes: 
您的安装可能需要执行其他配置步骤。

1. 以下产品需要安装支持的编译器:

Stateflow 9.2
Simulink Coder 9.0
MATLAB Coder 4.1
Simulink Test 2.5

2. Simulink 需要使用 C 编译器以实现仿真加速、模型引用和 MATLAB Function 模块功能。建议在您的计算机上安装支持的编译器。

3. 要加快以下产品的计算速度,需要安装支持的编译器:

SimBiology 5.8.1
Fixed-Point Designer 6.2

4. 此安装完成后,应按照从 www.mathworks.com/distconfig 获取的说明中所述继续配置 MATLAB Distributed Computing Server。

5. MATLAB Compiler SDK 6.6 要求安装以下程序:

● 支持的编译器,用于创建 C 和 C++ 共享库
● Java JDK,用于创建 Java 包

(二月 17, 2025 17:25:02) Exiting with status 0
(二月 17, 2025 17:25:04) End - Successful.
Finished

2025 年 2 月 18 日

1
2
(base) [root@thmn Crack]# sudo cp '/home/memory/matlab/Matlab_R2018b/Crack' '/home/memory/matlab/Matlab_R2018b/dev/licenses'
cp: 略过目录"/home/memory/matlab/Matlab_R2018b/Crack"

复制目录,搞错了。

1
(base) [root@thmn Crack]# sudo cp '/home/memory/matlab/Matlab_R2018b/Crack/license_standalone.lic' '/home/memory/matlab/Matlab_R2018b/dev/licenses'
1
(base) [root@thmn Crack]# sudo cp '/home/memory/matlab/Matlab_R2018b/Crack/bin/glnxa64/matlab_startup_plugins/lmgrimpl/libmwlmgrimpl.so' '/home/memory/matlab/Matlab_R2018b/dev/bin/glnxa64/matlab_startup_plugins/lmgrimpl'
1
rm -rf dev

vmware虚拟机连接服务器超时,vmware连接远程服务器超时-CSDN博客

1
2
3
4
5
6
7
使用FinalShell连接远程服务器后安装Matlab软件没有窗口弹出、没有安装页面引导,只有命令行的情况,通常是因为在远程服务器上运行图形界面软件时,受限于远程会话的环境,图形界面无法直接显示在本地计算机上。

针对这种问题,有几种可能的解决方案:

使用X11转发:
如果你的本地计算机和远程服务器都支持X11协议,你可以在SSH连接时启用X11转发。这样,远程服务器上运行的图形界面程序可以将图形输出发送到本地的X服务器进行显示。
在FinalShell中,你可以在SSH连接设置中找到并启用“X11转发”选项。然后,在远程服务器上运行Matlab安装程序时,图形界面应该会出现在你的本地计算机上。

转发图形界面。

记录一个Xshell使用中Xmanager…X11转发的提示问题_xshell x11-CSDN博客

FinalShell 不支持啊,只好安装 Xshell 了。

vmware虚拟机启动黑屏怎么解决_百度搜索 (baidu.com)

使用 Xshell 远程连接安装 Matlab 搞定了,但这边麒麟V10黑屏打不开了,试了一上午未果,索性直接重装虚拟机。

重装成功,安装 MCR 环境,运行。

1
2
3
4
5
6
memory@memory-pc:~/matlab/projects/helloworld$ ./run_helloworld.sh /usr/local/MATLAB/MATLAB_Runtime/v95
------------------------------------------
Setting up environment variables
---
LD_LIBRARY_PATH is .:/usr/local/MATLAB/MATLAB_Runtime/v95/runtime/glnxa64:/usr/local/MATLAB/MATLAB_Runtime/v95/bin/glnxa64:/usr/local/MATLAB/MATLAB_Runtime/v95/sys/os/glnxa64:/usr/local/MATLAB/MATLAB_Runtime/v95/sys/opengl/lib/glnxa64
Error: The installed MATLAB Runtime is not compatible with the application. Reinstall the application or get MATLAB Runtime from www.mathworks.com.

运行失败,环境不兼容,我得从 Matlab_R2018 软件里下载与之兼容的运行环境了。

编译器里下载编译环境,这下总该是合适的,然而安装运行环境的过程当中出现了一连串的报错。

1
mcrinstaller
1
compiler.runtime.download
1
2
3
4
5
>> compiler.runtime.download
Downloading MATLAB Runtime installer. It may take several minutes...

MATLAB Runtime installer has been downloaded to:
"/root/MCRInstaller9.5/MCR_R2018b_glnxa64_installer.zip"
1
./install -mode silent -agreeToLicense  yes

还有报错。

1
2
3
4
2025 14:47:45) Error: /usr/local/MATLAB/MATLAB_Runtime/v95/appdata/installedProductData.txt (权限不够)
(二月 18, 2025 14:47:45) Exiting with status -1
(二月 18, 2025 14:47:45) End - Unsuccessful.
Finished
1
cd /home/memory/mcr/

这都什么破问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
(二月 18, 2025 14:57:01) java.io.FileNotFoundException: /usr/local/MATLAB/MATLAB_Runtime/v95/appdata/installedProductData.txt (权限不够)
at java.io.FileOutputStream.open0(Native Method)
at java.io.FileOutputStream.open(FileOutputStream.java:270)
at java.io.FileOutputStream.(FileOutputStream.java:213)
at com.mathworks.instutil.FileIO.copyToFileFromStream(FileIO.java:509)
at com.mathworks.instutil.FileIO.createFileFromStream(FileIO.java:168)
at com.mathworks.instutil.FileIO.writeStringToFile(FileIO.java:351)
at com.mathworks.install_impl.InstalledProductDataImpl.writeInstalledProductData(InstalledProductDataImpl.java:260)
at com.mathworks.install_impl.InstallerImpl.install(InstallerImpl.java:142)
at com.mathworks.installwizard.model.InstallTask.execute(InstallTask.java:46)
at com.mathworks.installwizard.model.AbstractBackgroundTask.execute(AbstractBackgroundTask.java:38)
at com.mathworks.install_task.AbstractInstallTask.call(AbstractInstallTask.java:50)
at com.mathworks.install_task.AbstractInstallTask.call(AbstractInstallTask.java:18)
at com.mathworks.wizard.worker.WorkerImpl.doInBackground(WorkerImpl.java:24)
at javax.swing.SwingWorker$1.call(SwingWorker.java:295)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at javax.swing.SwingWorker.run(SwingWorker.java:334)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)

(二月 18, 2025 14:57:01) Error: /usr/local/MATLAB/MATLAB_Runtime/v95/appdata/installedProductData.txt (权限不够)
(二月 18, 2025 14:57:01) Exiting with status -1
(二月 18, 2025 14:57:01) End - Unsuccessful.
Finished

https://baijiahao.baidu.com/s?id=1820871759078867232&wfr=spider&for=pc

1
chmod -R 777 /path
1
./install -mode silent -agreeToLicense yes -destinationFoler /home/memory/mcr/MCR
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
(二月 18, 2025 15:21:08) java.io.FileNotFoundException: /usr/local/MATLAB/MATLAB_Runtime/v95/toolbox/local/classpath.txt (权限不够)
at java.io.FileOutputStream.open0(Native Method)
at java.io.FileOutputStream.open(FileOutputStream.java:270)
at java.io.FileOutputStream.(FileOutputStream.java:213)
at com.mathworks.instutil.FileIO.copyToFileFromStream(FileIO.java:509)
at com.mathworks.instutil.FileIO.createFileFromStream(FileIO.java:168)
at com.mathworks.instutil.FileIO.writeStringToFile(FileIO.java:351)
at com.mathworks.instutil.FileIO.writeStringToFile(FileIO.java:344)
at com.mathworks.install_impl.command.GenerateClasspathCommand.createClassPathTxt(GenerateClasspathCommand.java:92)
at com.mathworks.install_impl.command.GenerateClasspathCommand.undo(GenerateClasspathCommand.java:38)
at com.mathworks.install_impl.PreProductInstaller.uninstallProducts(PreProductInstaller.java:106)
at com.mathworks.install_impl.InstallerImpl.uninstallProducts(InstallerImpl.java:201)
at com.mathworks.install_impl.InstallerImpl.install(InstallerImpl.java:130)
at com.mathworks.installwizard.model.InstallTask.execute(InstallTask.java:46)
at com.mathworks.installwizard.model.AbstractBackgroundTask.execute(AbstractBackgroundTask.java:38)
at com.mathworks.install_task.AbstractInstallTask.call(AbstractInstallTask.java:50)
at com.mathworks.install_task.AbstractInstallTask.call(AbstractInstallTask.java:18)
at com.mathworks.wizard.worker.WorkerImpl.doInBackground(WorkerImpl.java:24)
at javax.swing.SwingWorker$1.call(SwingWorker.java:295)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at javax.swing.SwingWorker.run(SwingWorker.java:334)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)

(二月 18, 2025 15:21:08) Error: 应用程序遇到意外错误 并且需要关闭。您可能需要尝试重新安装产品。有关详细信息,请查看 /tmp/mathworks_root.log
(二月 18, 2025 15:21:08) Exiting with status -1
(二月 18, 2025 15:21:08) End - Unsuccessful.
Finished

这个报错跟文件权限没有任何关系。

setenforce 命令用于即时更改 SELinux(Security-Enhanced Linux)的安全策略执行模式。SELinux 是一个为 Linux 内核提供访问控制安全策略的安全模块。它允许系统管理员定义策略,这些策略决定了进程可以访问哪些文件、网络端口等资源。

setenforce 命令的参数说明如下:

  • Enforcing1:这是 SELinux 的默认模式,也称为强制模式。在这个模式下,SELinux 策略被严格执行。如果某个操作违反了策略规则,那么该操作将被阻止,并且通常会记录一条拒绝消息。
  • Permissive0:在这个模式下,SELinux 策略不会被强制执行。即使某个操作违反了策略规则,该操作仍然会被允许执行,但是会记录一条警告消息。这个模式通常用于故障排除或调试,因为它允许系统继续运行而不受 SELinux 策略的限制。

使用 setenforce 命令更改模式时,需要具有适当的权限,通常是以 root 用户身份运行。例如,要将 SELinux 设置为 Permissive 模式,可以使用以下命令:

1
2
3
bash复制代码

sudo setenforce 0

要将 SELinux 设置回 Enforcing 模式,可以使用:

1
2
3
bash复制代码

sudo setenforce 1

更改 SELinux 模式后,更改会立即生效,并且不需要重启系统。但是,请注意,将 SELinux 设置为 Permissive 模式会降低系统的安全性,因为它允许违反策略规则的操作执行。因此,只有在必要时才应该这样做,例如在故障排除期间。一旦问题解决,应该尽快将 SELinux 设置回 Enforcing 模式。

没什么用。

想起来之前有些问题,花了一年时间都没有解决掉。

指定安装位置。

1
./install -mode silent -agreeToLicense yes -destinationFolder /home/memory/mcr/MCR

不管怎么说,16 服务器上总算安装成功。

1
2
3
4
5
6
7
8
9
10
(二月 18, 2025 15:38:26) Notes: 
在目标计算机上,将以下内容追加到环境变量 LD_LIBRARY_PATH 的末尾:

/home/memory/mcr/MCR/v95/runtime/glnxa64:/home/memory/mcr/MCR/v95/bin/glnxa64:/home/memory/mcr/MCR/v95/sys/os/glnxa64:/home/memory/mcr/MCR/v95/extern/bin/glnxa64

如果 MATLAB Runtime 要与 MATLAB Production Server 配合使用,则您不需要修改上面的环境变量。

(二月 18, 2025 15:38:26) Exiting with status 0
(二月 18, 2025 15:38:26) End - Successful.
Finished

妈的,麒麟V10也总算安装成功。

1
2
3
4
5
6
7
8
9
在目标计算机上,将以下内容追加到环境变量 LD_LIBRARY_PATH 的末尾:

/home/memory/mcr/MCR/v95/runtime/glnxa64:/home/memory/mcr/MCR/v95/bin/glnxa64:/home/memory/mcr/MCR/v95/sys/os/glnxa64:/home/memory/mcr/MCR/v95/extern/bin/glnxa64

如果 MATLAB Runtime 要与 MATLAB Production Server 配合使用,则您不需要修改上面的环境变量。

(二月 18, 2025 15:39:54) Exiting with status 0
(二月 18, 2025 15:39:54) End - Successful.
Finished

配置环境变量。

1
sudo vim /etc/profile
1
/home/memory/mcr/MCR/v95
1
export LD_LIBRARY_PATH=/home/memory/mcr/MCR/v95/runtime/glnxa64:/home/memory/mcr/MCR/v95/bin/glnxa64:/home/memory/mcr/MCR/v95/extern/bin/glnxa64:$LD_LIBRARY_PATH
1
source /etc/profile
1
(base) [root@thmn analysis]# echo "$LD_LIBRARY_PATH"

麒麟这边又出什么问题。

1
2
3
4
5
6
7
8
9
root@memory-pc:/home/memory/mcr/MCR/v95# source /etc/profile
kysec_auth: /usr/local/MATLAB/MATLAB_Runtime/v95/bin/glnxa64/libQt5Core.so.5: no version information available (required by kysec_auth)
kysec_auth: /usr/local/MATLAB/MATLAB_Runtime/v95/bin/glnxa64/libQt5Core.so.5: no version information available (required by kysec_auth)
kysec_auth: /usr/local/MATLAB/MATLAB_Runtime/v95/bin/glnxa64/libQt5Core.so.5: no version information available (required by /lib/x86_64-linux-gnu/libQt5DBus.so.5)
kysec_auth: /usr/local/MATLAB/MATLAB_Runtime/v95/bin/glnxa64/libQt5Core.so.5: no version information available (required by /lib/x86_64-linux-gnu/libQt5DBus.so.5)
kysec_auth: /usr/local/MATLAB/MATLAB_Runtime/v95/bin/glnxa64/libQt5Core.so.5: no version information available (required by /lib/x86_64-linux-gnu/libQt5DBus.so.5)
kysec_auth: symbol lookup error: /lib/x86_64-linux-gnu/libQt5DBus.so.5: undefined symbol: _ZTI13QDaemonThread, version Qt_5_PRIVATE_API
-bash: source: kysec:权限不够:/etc/profile
root@memory-pc:/home/memory/mcr/MCR/v95# source /etc/profile

好像又没啥问题了。

16 这边执行。

1
2
3
4
5
6
(base) [root@thmn helloworld]# ./run_helloworld.sh /home/memory/mcr/MCR/v95
------------------------------------------
Setting up environment variables
---
LD_LIBRARY_PATH is .:/home/memory/mcr/MCR/v95/runtime/glnxa64:/home/memory/mcr/MCR/v95/bin/glnxa64:/home/memory/mcr/MCR/v95/sys/os/glnxa64:/home/memory/mcr/MCR/v95/sys/opengl/lib/glnxa64
Hello, World

他妈终于成功了。

麒麟这边还有点问题,运行不起来。

银河麒麟桌面操作系统V10SP1(全X86/ARM架构)【通过命令行关闭安全中心】操作方法 - 知乎 (zhihu.com)

1
getstatus
1
2
3
4
5
6
7
8
9
10
11
12
13
14
root@memory-pc:/home/memory/matlab/projects/helloworld# getstatus
KySec status: enabled

exec control : warning
net control : warning
file protect : on
kmod protect : on
three admin : off
process protect: on
device control: on
ipt control : on
kid protect : partition
program blklist: off
eperm control: off
1
setstatus disable

呵呵呵,显然麒麟V10也运行成功了。

1
2
3
4
5
6
root@memory-pc:/home/memory/matlab/projects/helloworld# ./run_helloworld.sh /home/memory/mcr/MCR/v95
------------------------------------------
Setting up environment variables
---
LD_LIBRARY_PATH is .:/home/memory/mcr/MCR/v95/runtime/glnxa64:/home/memory/mcr/MCR/v95/bin/glnxa64:/home/memory/mcr/MCR/v95/sys/os/glnxa64:/home/memory/mcr/MCR/v95/sys/opengl/lib/glnxa64
Hello, World

爷爷已经彻底解决了问题。

四点钟了,三个多小时总算给磨出来了。

等着,要其他测试数据。

2025 年 2 月 19 日

1
麒麟V10操作系统安装WPS等必要软件。
1
Xshell连接16服务器后使用Matlab编译打包工程文件为Shell脚本,在已安装MCR运行环境的麒麟V10操作系统中运行脚本,打通整个流程。
1
麒麟V10运行Matlab工程文件打包后的Shell脚本,成功导出预测数据至Excel表格。

继续!今天把这个工作整完!

很有意思。

在 Windows 本地接受测试代码并进行初步测试,再将测试工程文件通过 FinalShell 转发至16 服务器;

用 Xshell 连接 16 服务器打开安装完成的 Matlab 软件,编译打包工程文件为 Shell 脚本,再将生成的脚本文件下载到 Windows 本地;

经由 Windows 直接传输脚本文件到 VMware 虚拟机中早已成功安装 MCR 运行环境的麒麟V10操作系统;

最终在该系统成功运行脚本文件完成测试。

Matlab 和 mcr 文件夹下均保存安装包和软件本身,文件夹名需将二者区分开来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
root@memory-pc:/home/memory/mcr# ll
总用量 204
drwxrwxr-x 10 memory memory 4096 2月 19 15:54 ./
drwx------ 24 memory memory 4096 2月 19 15:22 ../
drwxr-xr-x 40 root root 167936 2月 18 15:12 archives/
drwxr-xr-x 3 root root 4096 2月 18 15:12 bin/
drwxrwxr-x 8 memory memory 4096 2月 19 15:55 installer/
drwxr-xr-x 5 root root 4096 2月 18 15:12 java/
drwxr-xr-x 3 root root 4096 2月 18 15:38 MCR/
drwxr-xr-x 2 root root 4096 2月 18 15:12 productdata/
drwxr-xr-x 4 root root 4096 2月 18 15:12 sys/
drwxr-xr-x 3 root root 4096 2月 18 15:12 ui/
root@memory-pc:/home/memory/mcr# rm -rf ./archives
root@memory-pc:/home/memory/mcr# rm -rf ./bin/
root@memory-pc:/home/memory/mcr# rm -rf ./java
root@memory-pc:/home/memory/mcr# rm -rf ./productdata/
root@memory-pc:/home/memory/mcr# rm -rf ./sys/
root@memory-pc:/home/memory/mcr# rm -rf ./ui/
1
2
3
4
5
6
root@memory-pc:/home/memory/mcr# ll
总用量 16
drwxrwxr-x 4 memory memory 4096 2月 19 16:00 ./
drwx------ 24 memory memory 4096 2月 19 15:22 ../
drwxrwxr-x 8 memory memory 4096 2月 19 15:55 installer/
drwxr-xr-x 3 root root 4096 2月 18 15:38 MCR/

Linux 下 Matlab 运行报错:

1
2
3
4
5
6
7
% 将各类型调节潜力数据存储到Excel文件中
writematrix(P_pot_rup_DG, 'DG-result.xlsx', 'Sheet', '向上调频可调节潜力');
writematrix(P_pot_rdown_DG, 'DG-result.xlsx', 'Sheet', '向下调频可调节潜力');
writematrix(P_pot_sr_DG, 'DG-result.xlsx', 'Sheet', '旋转备用可调节潜力');
writematrix(P_pot_nsr_DG, 'DG-result.xlsx', 'Sheet', '非旋转备用可调节潜力');
writematrix(V_pot_frrup_DG, 'DG-result.xlsx', 'Sheet', '向上快速调整率可调节潜力');
writematrix(V_pot_frrdown_DG, 'DG-result.xlsx', 'Sheet', '向下快速调整率可调节潜力');

在 MATLAB 中,writematrix 函数用于将矩阵数据写入到文件中,支持多种文件格式,包括 Excel 文件(.xlsx)。要使用 writematrix 函数将数据写入 Excel 文件,确实需要本地安装有 Microsoft Excel 或 WPS 等能够处理 Excel 文件的软件。这是因为 MATLAB 在写入 Excel 文件时,依赖于这些软件提供的 COM 接口或相关组件。

如果你没有安装 Excel 或 WPS,writematrix 函数将无法正常工作,并会抛出类似以下的错误:

1
Undefined function or variable 'writematrix'.

或者

1
2
Error using writematrix
Unable to start Excel server for export.
  1. 安装 Excel 或 WPS

    • 如果你需要将数据写入 Excel 文件,最简单的解决方案是安装 Microsoft Excel 或 WPS。安装后,MATLAB 将能够正常调用 writematrix 函数。
  2. 使用其他文件格式

    • 如果你不想安装 Excel 或 WPS,可以考虑将数据写入其他格式的文件,例如 CSV 文件。CSV 文件可以用文本编辑器或任何电子表格软件打开。你可以使用 writematrix 函数将数据写入 CSV 文件,如下所示:

      matlab

      复制

      1
      writematrix(P_pot_rup_DG, 'DG-result.csv');

      这样就不需要依赖 Excel 或 WPS。

  3. 使用 xlswrite 函数

    • 如果你使用的是较旧版本的 MATLAB(R2019a 之前),可以使用 xlswrite 函数来写入 Excel 文件。不过,xlswrite 同样需要本地安装 Excel 或 WPS。

      matlab

      复制

      1
      xlswrite('DG-result.xlsx', P_pot_rup_DG, '向上调频可调节潜力');
  4. **使用 writecellwritetable**:

    • 如果你的数据是单元格数组或表格,可以使用 writecellwritetable 函数来写入 Excel 文件。这些函数同样需要 Excel 或 WPS 的支持。

      matlab

      复制

      1
      writecell(P_pot_rup_DG, 'DG-result.xlsx', 'Sheet', '向上调频可调节潜力');

如果你需要将数据写入 Excel 文件,确保本地安装了 Microsoft Excel 或 WPS。如果没有安装这些软件,可以考虑将数据写入 CSV 文件或其他不需要依赖 Excel 的格式。

国产系统-银河麒麟桌面版安装wps - 浅水鲤鱼 - 博客园 (cnblogs.com)

银河麒麟V10如何安装本地deb软件包?(以安装wps为例)_银河麒麟安装deb安装包-CSDN博客

1
2
3
memory@memory-pc:~$ df -h .
文件系统 容量 已用 可用 已用% 挂载点
/dev/sda6 16G 8.1G 6.6G 56% /home

下载。

下载wps的.deb软件包。

免費下載 PC 版/Windows 版/Mac 版 WPS Office | 下載最新版本

安装。

使用dpkg安装。

1
sudo dpkg -i wps-office_11.1.0.11723.XA_amd64.deb
1
2
3
4
5
6
7
8
9
10
11
12
13
root@memory-pc:/home/memory/WPS# sudo dpkg -i wps-office_11.1.0.11723.XA_amd64.deb
正在选中未选择的软件包 wps-office。
(正在读取数据库 ... 系统当前共安装有 193187 个文件和目录。)
准备解压 wps-office_11.1.0.11723.XA_amd64.deb ...
正在解压 wps-office (11.1.0.11723.XA) ...
正在设置 wps-office (11.1.0.11723.XA) ...
正在处理用于 shared-mime-info (1.15-1kylin0k2.5) 的触发器 ...
正在处理用于 hicolor-icon-theme (0.17-2) 的触发器 ...
正在处理用于 fontconfig (2.13.1-2kylin3k0.2) 的触发器 ...
正在处理用于 desktop-file-utils (0.24-1kylin2) 的触发器 ...
正在处理用于 bamfdaemon (0.5.3+18.04.20180207.2-0kylin2) 的触发器 ...
Rebuilding /usr/share/applications/bamf-2.index...
正在处理用于 mime-support (3.64kylin1) 的触发器 ...

如果提示缺少依赖,继续下一步。

解决依赖问题(如果需要),这个命令会自动修复安装过程中可能遇到的依赖问题。

1
sudo apt-get install -f

虽然apt不直接支持安装本地.deb文件,但你可以通过dpkg安装,然后用apt-get install -f解决依赖。这样,你就能在银河麒麟V10上轻松安装本地.deb软件包了。

WPS 已经安装完毕。

接下来需要测试下导入 Excel 表是否成功,运行一下打包好的 Shell 脚本。

1
mcc -m doubleInput.m -o doubleInput
1
2
3
4
5
6
7
% 将各类型调节潜力数据存储到Excel文件中
writematrix(P_pot_rup_DG, 'DG-result.xlsx', 'Sheet', '向上调频可调节潜力');
writematrix(P_pot_rdown_DG, 'DG-result.xlsx', 'Sheet', '向下调频可调节潜力');
writematrix(P_pot_sr_DG, 'DG-result.xlsx', 'Sheet', '旋转备用可调节潜力');
writematrix(P_pot_nsr_DG, 'DG-result.xlsx', 'Sheet', '非旋转备用可调节潜力');
writematrix(V_pot_frrup_DG, 'DG-result.xlsx', 'Sheet', '向上快速调整率可调节潜力');
writematrix(V_pot_frrdown_DG, 'DG-result.xlsx', 'Sheet', '向下快速调整率可调节潜力');

测试下写文件,确实是新建 Excel 表格的。

1
mcc -m DG_potential1.m -o DG_potential1 -d D:\Project\tellhow\广西项目\分布式发电资源可调节潜力评估\DG_potential1

可以的。

顺便测试下打包后的代码能否正常连接到数据库。

1
mcc -m DG_potential2.m -o DG_potential2 -d D:\Project\tellhow\广西项目\分布式发电资源可调节潜力评估\DG_potential2

新建个测试数据库。

1
2
3
4
5
6
7
8
9
10
CREATE DATABASE DG_result_test;

CREATE TABLE P_pot_rup_DG_test (DG_ID INT, TIME INT, VALUE DOUBLE);
CREATE TABLE P_pot_rdown_DG_test (DG_ID INT, TIME INT, VALUE DOUBLE);
CREATE TABLE P_pot_sr_DG_test (DG_ID INT, TIME INT, VALUE DOUBLE);
CREATE TABLE P_pot_nsr_DG_test (DG_ID INT, TIME INT, VALUE DOUBLE);
CREATE TABLE V_pot_frrup_DG_test (DG_ID INT, TIME INT, VALUE DOUBLE);
CREATE TABLE V_pot_frrdown_DG_test (DG_ID INT, TIME INT, VALUE DOUBLE);

DROP DATABASE IF EXISTS DG_result_test2;

不对,连接数据库有点问题。

1
2
3
4
代码的运行顺序为"data_DG_timingchaincloud.m"-->"case_DG_timingchaincloud.m"-->"DG_potential1(2).m"
"DG-test.xlsx"为输入读取的测试数据文件(一天24*12个数据点)
"DG-result.xlsx"存储的为运行"DG_potential1.m"代码后的各类型调控潜力数据
"DG_potential2.m"使用 MATLAB 的 database 工具箱来连接 MySQL 数据库,并将数据写入数据库表中,实现功能与"DG_potential1.m"相同,仅在输出数据格式上存在差别。

测试下导出 Excel 表格。

1
2
3
4
>> mcc -m DG_potential1.m -o DG_potential1 -d /home/memory/matlab/Matlab_R2018b/projects/分布式发电资源可调节潜力评估/DG_potential1
The output directory
'/home/memory/matlab/Matlab_R2018b/projects/分布式发电资源可调节潜力评估/DG_potential1'
does not exist.

诶?

这样打包。

1
mcc -m DG_potential1.m -o DG_potential1 -d /home/memory/matlab/Matlab_R2018b/projects/分布式发电资源可调节潜力评估/

尝试运行。

1
cd /home/memory/matlab/Matlab_R2018b/projects/分布式发电资源可调节潜力评估/DG_potential
1
./run_DG_potential1.sh /home/memory/mcr/MCR/v95

出错。

1
2
3
未定义函数或变量 'writematrix'
出错 DG_potential1 (line 88)
MATLAB:UndefinedFunction

搞错了,涉及到 WPS,应该在麒麟V10上测试。

1
2
3
4
5
6
未定义函数或变量 'writematrix'

出错 DG_potential1 (line 88)

MATLAB:UndefinedFunction
root@memory-pc:/home/memory/matlab/projects/DG_potential#

哎哟卧槽,竟然是同样的问题,跟本机有无安装 WPS 没有关系。

1
在 MATLAB 中遇到错误 “未定义函数或变量 'writematrix'” 通常意味着 MATLAB 环境中没有找到名为 writematrix 的函数。writematrix 函数是 MATLAB R2019a 及更高版本中引入的一个新函数,用于将矩阵数据写入文件(如 CSV、TXT 等)。

艹。

奶奶的我的编译版本和运行版本是 matlab_R2018b,这家伙是 R2019a 引入的新函数。

可我还没有找到相关可用的其他免费 Linux Matlab 版本。。要不然谁会选择这么老版本的编译器。

在 MATLAB R2018b 版本中,虽然没有 writematrix 函数,但你可以使用 xlswrite 函数来将数据写入 Excel 文件。xlswrite 是 MATLAB 中较早引入的一个函数,用于将工作空间中的数据写入到 Excel 文件中。

以下是 xlswrite 函数的基本用法:

1
2
3
matlab

xlswrite(filename, M, sheet, range)
  • filename:要写入的 Excel 文件名,包括路径(如果文件不在当前工作目录中)。
  • M:要写入的 MATLAB 数组,可以是数值型、字符型或单元数组。
  • sheet(可选):要写入的工作表名称或索引。如果省略,数据将被写入活动工作表。
  • range(可选):要写入的 Excel 工作表中的单元格区域。如果省略,数据将从第一个单元格开始写入。

明天再看,下班愉快。

2025 年 2 月 20 日

1
解决Matlab_R2018b不兼容writematrix函数写入Excel文件的问题,使用writetable函数平替。
1
麒麟V10运行Matlab工程文件打包后的Shell脚本,成功导出预测数据至Excel表格。

改写下函数。

1
2
3
4
5
6
xlswrite(P_pot_rup_DG, 'DG-result.xlsx', 'Sheet', '���ϵ�Ƶ�ɵ���DZ��');
xlswrite(P_pot_rdown_DG, 'DG-result.xlsx', 'Sheet', '���µ�Ƶ�ɵ���DZ��');
xlswrite(P_pot_sr_DG, 'DG-result.xlsx', 'Sheet', '��ת���ÿɵ���DZ��');
xlswrite(P_pot_nsr_DG, 'DG-result.xlsx', 'Sheet', '����ת���ÿɵ���DZ��');
xlswrite(V_pot_frrup_DG, 'DG-result.xlsx', 'Sheet', '���Ͽ��ٵ����ʿɵ���DZ��');
xlswrite(V_pot_frrdown_DG, 'DG-result.xlsx', 'Sheet', '���¿��ٵ����ʿɵ���DZ��');
1
2
Error using xlswrite (line 170)
File name must be a string scalar or character vector.

看来不是函数问题,而是文件名命名问题。

1
2
3
4
Undefined function or variable 'writematrix'.

Error in DG_potential1 (line 88)
writematrix(P_pot_rup_DG, 'DG-result.xlsx', 'Sheet', '���ϵ�Ƶ�ɵ���DZ��');

正确的 xlswrite 用法应该像这样:

1
2
3
matlab复制代码

xlswrite(filename, data, sheet, range);

其中:

  • filename 是要写入的 Excel 文件名(包括路径,如果不在当前工作目录中)。
  • data 是要写入的 MATLAB 数组。
  • sheet(可选)是要写入的工作表名称或索引。
  • range(可选)是要写入的 Excel 工作表中的单元格区域。

表名,数据矩阵,Sheet,Sheet 名。

看来是乱码问题,粘贴还是不能解决,得解决编码问题。

修改MATLAB的默认编码方式 - weihy - 博客园 (cnblogs.com)

1
2
3
<encoding name="GBK">
<encoding_alias name="936"/>
</encoding>
1
2
3
<encoding name="UTF-8">
<encoding_alias name="utf8"/>
</encoding>
1
2
3
4
<encoding name="UTF-8"> 
<encoding_alias name="utf8"/>
<encoding_alias name="GBK"/>
</encoding>

MATLAB中文乱码问题解决方法:修改字体文件和编码方式-百度开发者中心 (baidu.com)

1
2
3
4
5
6
>> get(gca,'FontName')
Warning: MATLAB has disabled some advanced graphics rendering features by switching to software OpenGL. For more information, click here.

ans =

'Helvetica'

修改Matlab默认编码格式为UTF-8 - 知乎 (zhihu.com)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
feature('locale')

ans =
包含以下字段的 struct:
ctype: 'zh_CN.UTF-8'
collate: 'zh_CN.UTF-8'
time: 'zh_CN.UTF-8'
numeric: 'en_US_POSIX.UTF-8'
monetary: 'zh_CN.UTF-8'
messages: 'zh_CN.UTF-8'
encoding: 'UTF-8'
terminalEncoding: 'GBK'
jvmEncoding: 'UTF-8'
status: 'MathWorks locale management system initialized.'
warning: ''
1
2
3
4
5
6
7
8
9
10
11
12
13
ans = 
struct with fields:
ctype: 'en_US.UTF-8'
collate: 'en_US.UTF-8'
time: 'en_US.UTF-8'
numeric: 'en_US_POSIX.UTF-8'
monetary: 'en_US.UTF-8'
messages: 'en_US.UTF-8'
encoding: 'UTF-8'
terminalEncoding: 'UTF-8'
jvmEncoding: 'UTF-8'
status: 'MathWorks locale management system initialized.'
warning: ''

matlab_UTF-8编码设置_51CTO博客_python编码格式改为utf8

解决MATLAB2018b打开m文件后注释乱码的问题 (360doc.com)

1
2
3
4
5
>> slCharacterEncoding()


ans =
'UTF-8'

matlab中中文注释出现乱码问题(部分解决)_matlab中文乱码-CSDN博客

我直接把中文粘贴至 .txt 文本文档中,再粘贴到 .m 文件下,这样就能有中文了。

1
2
3
4
5
6
7
% 将各类型调节潜力数据存储到Excel文件中
xlswrite(P_pot_rup_DG, 'DG-result.xlsx', 'Sheet', '向上调频可调节潜力');
xlswrite(P_pot_rdown_DG, 'DG-result.xlsx', 'Sheet', '向下调频可调节潜力');
xlswrite(P_pot_sr_DG, 'DG-result.xlsx', 'Sheet', '旋转备用可调节潜力');
xlswrite(P_pot_nsr_DG, 'DG-result.xlsx', 'Sheet', '非旋转备用可调节潜力');
xlswrite(V_pot_frrup_DG, 'DG-result.xlsx', 'Sheet', '向上快速调整率可调节潜力');
xlswrite(V_pot_frrdown_DG, 'DG-result.xlsx', 'Sheet', '向下快速调整率可调节潜力');

还是有问题。

你的代码中 xlswrite 的参数顺序完全颠倒了。

正确语法应为:

1
xlswrite(FileName, Matrix, Sheet, Range)

即:

  1. 第一个参数:文件名(字符串)
  2. 第二个参数:要写入的数据矩阵
  3. 第三个参数:工作表名称或范围(可选)
  4. 第四个参数:写入范围(可选)
1
2
3
4
5
6
7
% 将各类型调节潜力数据存储到Excel文件中
xlswrite('DG-result.xlsx', P_pot_rup_DG, 'Sheet', '向上调频可调节潜力');
xlswrite('DG-result.xlsx', P_pot_rdown_DG, 'Sheet', '向下调频可调节潜力');
xlswrite('DG-result.xlsx', P_pot_sr_DG, 'Sheet', '旋转备用可调节潜力');
xlswrite('DG-result.xlsx', P_pot_nsr_DG, 'Sheet', '非旋转备用可调节潜力');
xlswrite('DG-result.xlsx', V_pot_frrup_DG, 'Sheet', '向上快速调整率可调节潜力');
xlswrite('DG-result.xlsx', V_pot_frrdown_DG, 'Sheet', '向下快速调整率可调节潜力');

这样执行又有报错了,没有成功写入 xlsx 文件。

1
2
3
4
5
6
7
8
9
10
11
12
writetable. 
> In xlswrite (line 179)
In DG_potential1 (line 91)
Warning: Unable to write to Excel format, attempting to write file to csv format. To write to an Excel file, convert your data to a table and use
writetable.
> In xlswrite (line 179)
In DG_potential1 (line 92)
Warning: Unable to write to Excel format, attempting to write file to csv format. To write to an Excel file, convert your data to a table and use
writetable.
> In xlswrite (line 179)
In DG_potential1 (line 93)
Elapsed time is 0.077989 seconds.

这样子尝试写,完美写入 Excel 文件,不过第一行数据有点问题。

1
2
3
% 方法一:使用 writetable(推荐)
data = table(P_pot_rup_DG);
writetable(data, 'DG-result.xlsx', 'Sheet', '向上调频可调节潜力', 'WriteVariableNames', true);

最终导出代码是这样子的。

1
2
3
4
5
6
7
8
9
10
11
12
13
% 使用 writetable(推荐)
P_pot_rup_DG = table(P_pot_rup_DG);
P_pot_rdown_DG = table(P_pot_rdown_DG);
P_pot_sr_DG = table(P_pot_sr_DG);
P_pot_nsr_DG = table(P_pot_nsr_DG);
V_pot_frrup_DG = table(V_pot_frrup_DG);
V_pot_frrdown_DG = table(V_pot_frrdown_DG);
writetable(P_pot_rup_DG, 'DG-result.xlsx', 'Sheet', '向上调频可调节潜力', 'WriteVariableNames', true);
writetable(P_pot_rdown_DG, 'DG-result.xlsx', 'Sheet', '向下调频可调节潜力', 'WriteVariableNames', true);
writetable(P_pot_sr_DG, 'DG-result.xlsx', 'Sheet', '旋转备用可调节潜力', 'WriteVariableNames', true);
writetable(P_pot_nsr_DG, 'DG-result.xlsx', 'Sheet', '非旋转备用可调节潜力', 'WriteVariableNames', true);
writetable(V_pot_frrup_DG, 'DG-result.xlsx', 'Sheet', '向上快速调整率可调节潜力', 'WriteVariableNames', true);
writetable(V_pot_frrdown_DG, 'DG-result.xlsx', 'Sheet', '向下快速调整率可调节潜力', 'WriteVariableNames', true);

导出的表格还有点问题,第一行填充了属性名,Sheet 前三个也是空白。

1
2
3
4
'WriteVariableNames', true 的作用
功能
当设置为 true 时,writetable 会将表格的列名(VariableNames)作为标题行写入文件(如 Excel 或 CSV)。
当设置为 false 时,writetable 仅写入数据,不写入列。

问题解决。不过下载到本地以后,前三个 Sheet 还是空白,先慢点看这个。

先打包再去麒麟运行试试。

1
mcc -m DG_potential1.m -o DG_potential1 -d /home/memory/matlab/Matlab_R2018b/projects/分布式发电资源可调节潜力评估/DG_potential/
1
./run_DG_potential1.sh /home/memory/mcr/MCR/v95
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
root@memory-pc:/home/memory/matlab/projects/DG_potential# ./run_DG_potential1.sh /home/memory/mcr/MCR/v95
------------------------------------------
Setting up environment variables
---
LD_LIBRARY_PATH is .:/home/memory/mcr/MCR/v95/runtime/glnxa64:/home/memory/mcr/MCR/v95/bin/glnxa64:/home/memory/mcr/MCR/v95/sys/os/glnxa64:/home/memory/mcr/MCR/v95/sys/opengl/lib/glnxa64

警告: 已添加指定的工作表。
> In tabular/writeXLSFile>getSheetFromBook (line 260)
In tabular/writeXLSFile (line 14)
In table/write (line 164)
In writetable (line 142)
In DG_potential1 (line 102)
警告: 已添加指定的工作表。
> In tabular/writeXLSFile>getSheetFromBook (line 260)
In tabular/writeXLSFile (line 14)
In table/write (line 164)
In writetable (line 142)
In DG_potential1 (line 103)
警告: 已添加指定的工作表。
> In tabular/writeXLSFile>getSheetFromBook (line 260)
In tabular/writeXLSFile (line 14)
In table/write (line 164)
In writetable (line 142)
In DG_potential1 (line 104)
警告: 已添加指定的工作表。
> In tabular/writeXLSFile>getSheetFromBook (line 260)
In tabular/writeXLSFile (line 14)
In table/write (line 164)
In writetable (line 142)
In DG_potential1 (line 105)
警告: 已添加指定的工作表。
> In tabular/writeXLSFile>getSheetFromBook (line 260)
In tabular/writeXLSFile (line 14)
In table/write (line 164)
In writetable (line 142)
In DG_potential1 (line 106)
警告: 已添加指定的工作表。
> In tabular/writeXLSFile>getSheetFromBook (line 260)
In tabular/writeXLSFile (line 14)
In table/write (line 164)
In writetable (line 142)
In DG_potential1 (line 107)
时间已过 0.723751 秒。

很显然执行成功了,我的朋友们。

成功导出 Excel 表格文件,输出内容正常。

1
2
3
4
5
6
7
8
emory  881 2月  20 11:09 run_DG_potential1.sh*
root@memory-pc:/home/memory/matlab/projects# ./run_DG_potential1.sh /home/memory/mcr/MCR/v95
------------------------------------------
Setting up environment variables
---
LD_LIBRARY_PATH is .:/home/memory/mcr/MCR/v95/runtime/glnxa64:/home/memory/mcr/MCR/v95/bin/glnxa64:/home/memory/mcr/MCR/v95/sys/os/glnxa64:/home/memory/mcr/MCR/v95/sys/opengl/lib/glnxa64
./run_DG_potential1.sh: 1: eval: ./DG_potential1: not found
root@memory-pc:/home/memory/matlab/projects#

看来打包生成后的这几个文件,必须全部保留在同一目录下,才能执行成功。

那么只要将每个 matlab 工程文件打包一份,就能完成任务了。

1
mcc -m data_DG_timingchaincloud.m -o data_DG_timingchaincloud -d /home/memory/matlab/Matlab_R2018b/projects/分布式发电资源可调节潜力评估/data_DG_timingchaincloud/
1
mcc -m case_DG_timingchaincloud.m -o case_DG_timingchaincloud -d /home/memory/matlab/Matlab_R2018b/projects/分布式发电资源可调节潜力评估/case_DG_timingchaincloud/

启动。

1
./run_data_DG_timingchaincloud.sh /home/memory/mcr/MCR/v95
1
./run_case_DG_timingchaincloud.sh /home/memory/mcr/MCR/v95
1
2
3
4
5
6
7
oot@memory-pc:/home/memory/matlab/projects/case_DG_timingchaincloud# ./run_case_DG_timingchaincloud.sh /home/memory/mcr/MCR/v95
------------------------------------------
Setting up environment variables
---
LD_LIBRARY_PATH is .:/home/memory/mcr/MCR/v95/runtime/glnxa64:/home/memory/mcr/MCR/v95/bin/glnxa64:/home/memory/mcr/MCR/v95/sys/os/glnxa64:/home/memory/mcr/MCR/v95/sys/opengl/lib/glnxa64
未定义函数或变量 'P_DG_history'
出错 case_DG_timingchaincloud (line 7)

这样子保存数据。

1
2
% 在 data_DG_timingchaincloud.m 结尾保存数据
save('data_DG_timingchaincloud.mat', 'T', 'N_DG', 'H_DG', 'P_DG_min', 'P_DG_max', 'P_DG_history');
1
2
% 加载预编译的数据文件
load('../data_DG_timingchaincloud.mat');
1
2
3
4
5
6
7
LD_LIBRARY_PATH is .:/home/memory/mcr/MCR/v95/runtime/glnxa64:/home/memory/mcr/MCR/v95/bin/glnxa64:/home/memory/mcr/MCR/v95/sys/os/glnxa64:/home/memory/mcr/MCR/v95/sys/opengl/lib/glnxa64
错误使用 load
无法读取文件 '../data_DG_timingchaincloud.mat'。没有此类文件或目录。

出错 case_DG_timingchaincloud (line 4)

MATLAB:load:couldNotReadFile

看来只能给出文件根路径了,直接在麒麟上尝试测试。

1
/home/memory/matlab/projects
1
2
% 加载预编译的数据文件
load('/home/memory/matlab/projects/data_DG_timingchaincloud.mat');
1
2
3
4
5
LD_LIBRARY_PATH is .:/home/memory/mcr/MCR/v95/runtime/glnxa64:/home/memory/mcr/MCR/v95/bin/glnxa64:/home/memory/mcr/MCR/v95/sys/os/glnxa64:/home/memory/mcr/MCR/v95/sys/opengl/lib/glnxa64
错误使用 load
无法读取文件 '/home/memory/matlab/projects/data_DG_timingchaincloud.mat'。没有此类文件或目录。

出错 case_DG_timingchaincloud (line 4)

艹,写错了。

1
2
% 加载预编译的数据文件
load('/home/memory/matlab/projects/data_DG_timingchaincloud/data_DG_timingchaincloud.mat');

又有问题。

1
2
3
4
5
LD_LIBRARY_PATH is .:/home/memory/mcr/MCR/v95/runtime/glnxa64:/home/memory/mcr/MCR/v95/bin/glnxa64:/home/memory/mcr/MCR/v95/sys/os/glnxa64:/home/memory/mcr/MCR/v95/sys/opengl/lib/glnxa64
未定义函数或变量 'case_DG_timingchaincloud'

MATLAB:UndefinedFunction
Error: 未定义函数或变量 'case_DG_timingchaincloud'

为什么呢,加载到了数据就可以,怎么就未定义函数了。

再次打包一遍,结果运行后没这个问题了,莫名其妙。

1
2
% 在 data_DG_timingchaincloud.m 结尾保存数据
save('data_DG_timingchaincloud.mat', 'T', 'N_DG', 'H_DG', 'P_DG_min', 'P_DG_max', 'L_DG', 'P_DG_history');

三个文件都执行成功了,看来导出 Excel 表格初步测试完成,不过第一步读取表格为什么没有报错。

1
2
%分布式发电历史数据
P_DG_history=xlsread('DG-test.xlsx');

接下来测试数据库连接及数据写入。

两方面:本地打包后的可执行文件运行后,能正常连接到数据库;Linux 下 Matlab 正常连接数据库。

我觉着这里,打包后会有问题。

1
2
% 连接 MySQL 数据库
javaaddpath("E:\Matlab\java\jar\toolbox\mysql\mysql-connector-java-8.0.28.jar")

2025 年 2 月 21 日

1
mcc -m DG_potential2.m -o DG_potential2 -d D:\Project\tellhow\广西项目\分布式发电资源可调节潜力评估\DG_potential2
1
D:\Project\tellhow\广西项目\分布式发电资源可调节潜力评估
1
2
3
% 加载数据
% load DG-test2; % 调用一维云模型算法的计算结果
load('D:\Project\tellhow\广西项目\分布式发电资源可调节潜力评估\DG-test2.mat')
1
2
3
% 连接 MySQL 数据库
% javaaddpath("E:\Matlab\java\jar\toolbox\mysql\mysql-connector-java-8.0.28.jar")
javaaddpath("D:\Project\tellhow\广西项目\分布式发电资源可调节潜力评估\DG_potential2\mysql-connector-java-8.0.28.jar")

改完加载数据路径和 JDBC 驱动路径后,才发现一直以来报错的信息是这样的:

1
MCR:mclmcr:MCLMCR_Invalid_MATLAB_Runtime

本机上竟然出现了运行环境错误问题。

是否需要直接放弃本机测试,直接在麒麟V10尝试打通流程。

连接远程数据库成功。

1
2
3
4
5
6
7
8
9
10
11
12
13
databasename = "DG_result";
conn = database(databasename,"root","root",'Vendor','MySQL', ...
'Server','192.168.118.118','PortNumber',3306,'LoginTimeout',5);

disp(conn);


% 检查连接是否成功
if isopen(conn)
disp('数据库连接成功!');
else
error('数据库连接失败!');
end

同样的代码,在 Linux Matlab 上测试。

1
2
3
4
5
6
7
8
9
10
databasename = "iois_backend";
javaaddpath("/home/memory/matlab/Matlab_R2018b/dev/Matlab_R2018b/java/jar/toolbox/mysql/mysql-connector-java-8.0.28.jar")
conn = database(databasename,"root","root",'Vendor','MySQL', ...
'Server','192.168.118.118','PortNumber',3306,'LoginTimeout',5);

disp(conn);

selectquery = "SELECT * FROM license_info";
data = select(conn,selectquery);
disp(data);

一步到位,连接远程数据库成功。

下一步,连接数据库后,尝试能否正常写入预测数据到数据库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
% MySQL 
javaaddpath("/home/memory/matlab/Matlab_R2018b/dev/Matlab_R2018b/java/jar/toolbox/mysql/mysql-connector-java-8.0.28.jar")

databasename = "DG_result";
conn = database(databasename,"root","root",'Vendor','MySQL', ...
'Server','192.168.118.118','PortNumber',3306,'LoginTimeout',5);

disp(conn);

% 检查连接是否成功
if isopen(conn)
disp('数据库连接成功!');
else
error('数据库连接失败!');
end

漂亮,又是一步到位,成功连接数据库并导出预测数据至远程MySQL数据库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.
connection with properties:

DataSource: 'DG_result'
UserName: 'root'
Driver: 'com.mysql.jdbc.Driver'
URL: 'jdbc:mysql://192.168.118. ...'
Message: ''
Type: 'JDBC Connection Object'
Database Properties:

AutoCommit: 'on'
ReadOnly: 'off'
LoginTimeout: 5
MaxDatabaseConnections: 0

Catalog and Schema Information:

DefaultCatalog: 'DG_result'
Catalogs: {'dg_result', 'information_schema', 'iois_backend' ... and 5 more}
Schemas: {}

Database and Driver Information:

DatabaseProductName: 'MySQL'
DatabaseProductVersion: '8.0.40'
DriverName: 'MySQL Connector/J'
DriverVersion: 'mysql-connector-java-8.0. ...'

数据库连接成功!

再下一步,测试打包后能否正常连接到数据库,jar 包什么的都准备完善。

1
mcc -m DG_potential2.m -o DG_potential2 -d /home/memory/matlab/Matlab_R2018b/projects/分布式发电资源可调节潜力评估/DG_potential2

打包后,尝试运行。

1
./run_DG_potential2.sh /home/memory/mcr/MCR/v95

很好,出现新问题了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
95/sys/opengl/lib/glnxa64

警告: 文件或目录 '/home/memory/matlab/Matlab_R2018b/projects/分布式发电资源可调节潜力评估/DG_potential2/mysql-connector-java-8.0.28.jar' 无效。
> In javaclasspath
In javaclasspath
In javaclasspath
In javaaddpath (line 71)
In DG_potential2 (line 90)
connection - 属性:

DataSource: ''
UserName: ''
Driver: ''
URL: ''
Message: 'No suitable driver found ...'
Type: 'JDBC Connection Object'
Database Properties:

AutoCommit: ''
ReadOnly: ''
LoginTimeout: 0
MaxDatabaseConnections: -1

Catalog and Schema Information:

DefaultCatalog: ''
Catalogs: {}
Schemas: {}

Database and Driver Information:

DatabaseProductName: ''
DatabaseProductVersion: ''
DriverName: ''
DriverVersion: ''

错误使用 DG_potential2 (line 102)
数据库连接失败!

修改下 jar 包路径,再次测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
root@memory-pc:/home/memory/matlab/projects/DG_potential2# ./run_DG_potential2.sh /home/memory/mcr/MCR/v95
------------------------------------------
Setting up environment variables
---
LD_LIBRARY_PATH is .:/home/memory/mcr/MCR/v95/runtime/glnxa64:/home/memory/mcr/MCR/v95/bin/glnxa64:/home/memory/mcr/MCR/v95/sys/os/glnxa64:/home/memory/mcr/MCR/v95/sys/opengl/lib/glnxa64

Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.
connection - 属性:

DataSource: 'DG_result'
UserName: 'root'
Driver: 'com.mysql.jdbc.Driver'
URL: 'jdbc:mysql://192.168.118. ...'
Message: ''
Type: 'JDBC Connection Object'
Database Properties:

AutoCommit: 'on'
ReadOnly: 'off'
LoginTimeout: 5
MaxDatabaseConnections: 0

Catalog and Schema Information:

DefaultCatalog: 'DG_result'
Catalogs: {'dg_result', 'information_schema', 'iois_backend' ... and 5 more}
Schemas: {}

Database and Driver Information:

DatabaseProductName: 'MySQL'
DatabaseProductVersion: '8.0.40'
DriverName: 'MySQL Connector/J'
DriverVersion: 'mysql-connector-java-8.0. ...'

数据库连接成功!

时间已过 26.505150 秒。

很显然成功了。

上周二至这周五的工作,基本完成。

我是不是应该为未来做好相对充足的准备。

1
@回忆如初 172.16.6.33:22330,root/Tellhow@2020 你先把matlab的程序部署到这台服务器上去吧,然后写一下部署手册。业务调用的话周一我再跟你说下

很好,又有新的任务了,总不能让我闲出毛病来吧。

目录下很乱,清理一下。

下午,执行 Matlab 安装。

查看当前目录占用磁盘空间大小。

1
du -sh

查看当前目录剩余磁盘空间大小。

1
df -h .

指定安装目录。

1
./install -mode silent -agreeToLicense yes -fileInstallationKey 09806-07443-53955-64350-21751-41297 -destinationFolder /home/memory/matlab/Matlab_R2018b/dev

开始安装了。

1
192.168.118.120      th    th123456
1
192.168.118.16  root    rootmax
1
172.16.6.33:22330	root   Tellhow@2020
1
172.16.6.33:9000  guangxi 	@Wtellhow123! 
1
172.16.6.33:22330	root	Tellhow@2020 

什么破服务器,远程文件传输 Xftp 连接失败也就算了,竟然手动传输文件也失败,本来硬盘空间就小,现在更是没法操作了。

上传失败

2025 年 2 月 21 日

linux上传文件失败的问题-CSDN博客

从本地上传文件到Linux服务器的上传失败问题解决_linux上传文件失败-CSDN博客

1
2
3
4
5
6
7
8
9
C:\WINDOWS\system32>scp D:\桌面\网络与信息管理员.png th@192.168.118.120:/home/memory/matlab
The authenticity of host '192.168.118.120 (192.168.118.120)' can't be established.
ED25519 key fingerprint is SHA256:o7sP0IoZDt/wXles2I+T+aDM59bnFDiPoh9GcTQdqXY.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])?
Warning: Permanently added '192.168.118.120' (ED25519) to the list of known hosts.
th@192.168.118.120's password:
scp: dest open "/home/memory/matlab/\347\275\221\347\273\234\344\270\216\344\277\241\346\201\257\347\256\241\347\220\206\345\221\230.png": Permission denied
scp: failed to upload file D:/\346\241\214\351\235\242/\347\275\221\347\273\234\344\270\216\344\277\241\346\201\257\347\256\241\347\220\206\345\221\230.png to /home/memory/matlab
1
2
3
4
C:\WINDOWS\system32>scp D:\桌面\OutputFile.txt th@192.168.118.120:/home/memory/matlab
th@192.168.118.120's password:
scp: dest open "/home/memory/matlab/OutputFile.txt": Permission denied
scp: failed to upload file D:/\346\241\214\351\235\242/OutputFile.txt to /home/memory/matlab

原来是非管理员,没有权限。

1
root	th123456
1
2
3
4
5
C:\WINDOWS\system32>scp D:\桌面\OutputFile.txt root@192.168.118.120:/home/memory/matlab
root@192.168.118.120's password:
Permission denied, please try again.
root@192.168.118.120's password:
OutputFile.txt 100% 291 31.6KB/s 00:00
1
2
3
C:\WINDOWS\system32>scp D:\桌面\网络与信息管理员.png root@192.168.118.120:/home/memory/matlab
root@192.168.118.120's password:
网络与信息管理员.png 100% 167KB 3.6MB/s 00:00
1
scp D:\桌面\网络与信息管理员.png root@172.16.6.33:22330:/sys/fs/cgroup

尝试直接传输,又出问题。

1
2
3
C:\WINDOWS\system32>scp D:\桌面\网络与信息管理员.png root@172.16.6.33:22330:/sys/fs/cgroup
ssh: connect to host 172.16.6.33 port 22: Connection refused
scp: Connection closed

艹。

1
2
3
C:\WINDOWS\system32>scp D:\桌面\网络与信息管理员.png root@172.16.6.33:22330:/home/memory
ssh: connect to host 172.16.6.33 port 22: Connection refused
scp: Connection closed

任何办法到这台服务器下就不灵了。

什么破服务器,我直接在 120 上部署下 Matlab 吧。

1
scp E:\文件下载位置\百度网盘下载位置\MathWorks.MATLAB.R2018blinux.zip root@192.168.118.120:/home/memory/matlab

挺快的,二十分钟不到就已经上传了93%。

下班前的最后几分钟,传输完成了。

剩下的下周再看吧。

我的朋友,这一周辛苦了。

周末愉快。

部署文档

2025 年 2 月 25 日

Matlab 安装

Matlab 软件安装至 Linux服务器。

下载安装包,资源地址:

1
2
百度网盘链接:https://pan.baidu.com/s/1ka6MauAPAaxcfs3L7XUMOQ
提取码:za1u

执行解压。

1
unzip MathWorks.MATLAB.R2018blinux.zip -d /home/memory/matlab/Matlab_R2018b/installer

解压成功,安装前赋予权限。

1
cd /home/memory/matlab/Matlab_R2018b/installer
1
sudo chmod 777 install 
1
sudo chmod 777 /home/memory/matlab/Matlab_R2018b/installer/bin/glnxa64/install_unix
1
chmod +x /home/memory/matlab/Matlab_R2018b/installer/sys/java/jre/glnxa64/jre/bin/java

安装 Xmanager 6,使用Xshell 开启 X11 服务后远程连接服务器,使用窗口交互页面执行安装 Matlab。

直接执行安装。

1
./install

选择使用文件安装密钥安装;输入文件秘钥;选择安装位置;等待安装完成。

安装成功后,复制缺失文件至安装根目录下。

打开 Crack 文件夹,复制其下的 license_standalone.lic 文件至安装目录的 licenses 文件夹下。

1
sudo cp '/home/memory/matlab/Matlab_R2018b/Crack/license_standalone.lic' '/home/memory/matlab/Matlab_R2018b/dev/licenses'

再次打开 Crack 文件夹,依次打开 bin\glnxa64\matlab_startup_plugins\lmgrimpl 文件夹,复制其下的文件至安装目录的同名文件夹下。

1
sudo cp '/home/memory/matlab/Matlab_R2018b/Crack/bin/glnxa64/matlab_startup_plugins/lmgrimpl/libmwlmgrimpl.so' '/home/memory/matlab/Matlab_R2018b/dev/bin/glnxa64/matlab_startup_plugins/lmgrimpl'

打开 Matlab。

1
cd /home/memory/matlab/Matlab_R2018b/dev/Matlab_R2018b/bin
1
./matlab

MCR 配置

安装兼容该 Matlab 编译器版本的 MCR ,需在 Matlab 命令行窗口下输入以下命令获取可用的 MCR 压缩包路径。

1
mcrinstaller

若 MCR 路径为空,则手动执行下载 MCR 安装包。

1
compiler.runtime.download

等待下载完成,获取到 MCR 压缩包路径,解压至指定目录。

1
unzip MCR_R2018b_glnxa64_installer -d /home/memory/mcr/MCR_R2018b/installer

解压完成,直接执行,静默安装。

1
cd /home/memory/mcr/MCR_R2018b/installer
1
sudo ./install -mode silent -agreeToLicense  yes -destinationFoler /home/memory/mcr/MCR_R2018b/env

将以下内容追加到环境变量 LD_LIBRARY_PATH 的末尾。

1
sudo vim /etc/profile
1
export LD_LIBRARY_PATH=/usr/local/MATLAB/MATLAB_Runtime/v95/runtime/glnxa64:/usr/local/MATLAB/MATLAB_Runtime/v95/bin/glnxa64:/usr/local/MATLAB/MATLAB_Runtime/v95/extern/bin/glnxa64:$LD_LIBRARY_PATH

立即生效更新内容,检验环境变量是否修改成功。

1
source /etc/profile
1
echo $LD_LIBRARY_PATH

Matlab 已经安装成功,MCR 配置成功,尝试编译下工程文件。

1
mcc -m helloworld.m -o helloworld -d /home/memory/matlab/Matlab_R2018b/projects/helloworld

运行编译生成的脚本。

1
cd /home/memory/matlab/Matlab_R2018b/projects/helloworld
1
./run_helloworld.sh /home/memory/mcr/MCR_R2018b/env/v95

代码运行

拉取代码:guangxi-vpp-main - Repos (tellhowsoft.com)

将代码文件中的data_DG_timingchaincloudcase_DG_timingchaincloudDG_potential1DG_potential2文件夹以及run_all.sh脚本文件,上传至麒麟V10服务器。

执行脚本。

1
./run_all.sh /home/memory/mcr/MCR_R2018b/env/v95

成功连接远程数据库并新增数据记录,成功导出DG-result.xlsx文件至同级目录下。

2025 年 2 月 24 日

很快又到周一了,新的一周又开始了,过去的日子果然又追赶了上来。

33 服务器上传文件还有点问题,解压就更不必说了。

再试一试。

艹。

部署文档还没写呢。

word字体宋体(正文)和宋体(标题)二者有什么不同?-ZOL问答

如何在WPS文档中插入代码块:步骤与技巧-WPS高效文档技巧使用方法 (kdocs.cn)

如何在WPS文档中高效插入代码块-WPS高效文档技巧使用方法 (kdocs.cn)

特么 TFS 登陆不上。。。

跑一遍。

1
/run_data_DG_timingchaincloud.sh /home/memory/mcr/MCR/v95
1
./run_case_DG_timingchaincloud.sh /home/memory/mcr/MCR/v95
1
./run_DG_potential1.sh  /home/memory/mcr/MCR/v95
1
./run_DG_potential2.sh  /home/memory/mcr/MCR/v95

新增执行文件。

1
touch run_all.sh
1
vim run_all.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/bin/bash

echo "开始执行 /run_data_DG_timingchaincloud.sh"
./data_DG_timingchaincloud/run_data_DG_timingchaincloud.sh /home/memory/mcr/MCR/v95
echo "/run_data_DG_timingchaincloud.sh 执行完成"

echo "开始执行 ./run_case_DG_timingchaincloud.sh"
/case_DG_timingchaincloud/run_case_DG_timingchaincloud.sh /home/memory/mcr/MCR/v95
echo "./run_case_DG_timingchaincloud.sh 执行完成"

echo "开始执行 ./run_DG_potential1.sh"
./DG_potential1/run_DG_potential1.sh /home/memory/mcr/MCR/v95
echo "./run_DG_potential1.sh 执行完成"

echo "开始执行 ./run_DG_potential2.sh"
/DG_potential2/run_DG_potential2.sh /home/memory/mcr/MCR/v95
echo "./run_DG_potential2.sh 执行完成"

1
chmod +x run_all.sh

下午了。

代码仓登陆失败,推不上去。

至少今天上午把代码整理完毕,写了一个脚本依次执行四个工程文件。

罗列下需要解决的问题:

推送代码,登录 TFS;解决 33 服务器文件上传下载问题,安装 MCR 环境;论文;毕设;完成新的测试。

试一下本机直接传输。

1
192.168.118.120      th    th123456
1
192.168.118.16  root    rootmax
1
172.16.6.33:22330	root   Tellhow@2020
1
172.16.6.33:9000  guangxi 	@Wtellhow123! 
1
172.16.6.33:22330	root	Tellhow@2020 

IDEA 连接 33 失败。

1
2
3
4
5
C:\WINDOWS\system32>ping 172.16.6.33:9000
Ping 请求找不到主机 172.16.6.33:9000。请检查该名称,然后重试。

C:\WINDOWS\system32>ping 172.16.6.33:22330
Ping 请求找不到主机 172.16.6.33:22330。请检查该名称,然后重试。

本机 Ping 33 同样失败。

特么到底该怎么上传文件。

这样还真成功了,在 16 服务器里远程连接 33 服务器。

1
ssh -p 22330 root@172.16.6.33
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(base) [root@thmn ~]# ssh -p 22330 root@172.16.6.33
The authenticity of host '[172.16.6.33]:22330 ([172.16.6.33]:22330)' can't be established.
ECDSA key fingerprint is SHA256:sFWcG4VMGv3ZPQXrNYgaiLX4sh+iNSRCrj1fjpr4QsY.
ECDSA key fingerprint is MD5:ef:9e:40:92:14:ba:74:03:6d:10:5e:9e:a0:f6:f2:cf.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '[172.16.6.33]:22330' (ECDSA) to the list of known hosts.
root@172.16.6.33's password:
Last login: Mon Feb 24 14:36:04 2025 from 192.168.116.90
[root@sesp-test-1 ~]# ll
总用量 207928
-rw-r--r-- 1 root root 274 2月 14 15:45 1711526364355_ctrlCurve_2024_3_21_15_29.e
-rw-------. 1 root root 1349 9月 27 2020 anaconda-ks.cfg
drwxr-xr-x 11 root root 157 12月 11 2023 cloudx
drwxr-xr-x 3 root root 17 11月 5 2021 docker-images
-rw-r--r-- 1 root root 212520448 11月 18 14:32 fastdfs.tar
drwxr-xr-x 5 root root 50 10月 20 2023 logs
drwxr-xr-x 7 root root 70 7月 3 2024 nacos
-rw-r--r-- 1 root root 380772 9月 5 2021 nginx-1.8.1-1.el7.ngx.x86_64.rpm
drwxr-xr-x 2 root root 6 10月 18 2023 package
-rw-r--r-- 1 root root 45 9月 6 2021 sesp-isc-web.tar.gz
-rw-r--r-- 1 root root 2848 1月 11 2022 test.txt
drwxr-xr-x 3 root root 18 10月 18 2023 volumes
drwxr-xr-x 2 root root 6 9月 10 2021 zh

登出。

1
2
3
4
[root@sesp-test-1 ~]# exit
登出
Connection to 172.16.6.33 closed.
(base) [root@thmn ~]# ll

现在链接最有趣的是,用 FinalShell 连接 33 服务器 22330 没有问题,但上传下载文件失败,尝试本机、Xshell、Xftp 连接都特么失败了。

【2024最新版可用,可用到2099】详解可成功的idea一键激活,(附安装包&激活文件) - 哔哩哔哩 (bilibili.com)

有趣,IDEA 又快特么过期了。

明天再看这个。

探讨几种在CentOS 7上实现文件上传的方法_centos7上传本地文件-CSDN博客

艹。没法下手。

研究会儿论文。

1
vim /etc/selinux/config
1
cd /root

换个目录竟真的能成功开始上传运行环境,接下来就准备安装了。

等待上传中。

解压。

1
unzip MCR_R2018b_glnxa64_installer -d /home/memory/mcr/MCR_R2018b

期待解压成功。

1
sudo ./install -mode silent -agreeToLicense  yes
1
sudo vim /etc/profile
1
export LD_LIBRARY_PATH=/usr/local/MATLAB/MATLAB_Runtime/v95/runtime/glnxa64:/usr/local/MATLAB/MATLAB_Runtime/v95/bin/glnxa64:/usr/local/MATLAB/MATLAB_Runtime/v95/extern/bin/glnxa64:$LD_LIBRARY_PATH
1
source /etc/profile
1
echo $LD_LIBRARY_PATH

一条龙 MCR 安装步骤。

安装很顺利嘛。

1
2
3
4
5
如果 MATLAB Runtime 要与 MATLAB Production Server 配合使用,则您不需要修改上面的环境变量。

(二月 24, 2025 16:36:55) Exiting with status 0
(二月 24, 2025 16:36:55) End - Successful.
Finished

尝试测试。

1
./run_helloworld.sh  /usr/local/MATLAB/MATLAB_Runtime/v95
1
2
3
4
5
6
7
8
[root@sesp-test-1 ~]# ./run_helloworld.sh  /usr/local/MATLAB/MATLAB_Runtime/v95
------------------------------------------
Setting up environment variables
---
LD_LIBRARY_PATH is .:/usr/local/MATLAB/MATLAB_Runtime/v95/runtime/glnxa64:/usr/local/MATLAB/MATLAB_Runtime/v95/bin/glnxa64:/usr/local/MATLAB/MATLAB_Runtime/v95/sys/os/glnxa64:/usr/local/MATLAB/MATLAB_Runtime/v95/sys/opengl/lib/glnxa64

Dynamic exception type: std::runtime_error
std::exception::what: Bundle#7 start failed: libXt.so.6: 无法打开共享对象文件: 没有那个文件或目录

为什么。

这台服务器真的别扭,磁盘空间不大不说,上传文件,新建文件夹都磨磨蹭蹭的,实在不想再远程连接这家伙了。

在Xftp中给CentOS传送文件老是失败?_centos7 xftp无法上传-CSDN博客

2025 年 2 月 25 日

昨天下午安装后竟然运行失败了,今天尝试安装到别的目录。

1
./install -mode silent -agreeToLicense yes -destinationFolder /home/memory/mcr/MCR_R2018b/env

这还有好心提醒呢。

1
2
3
在目标计算机上,将以下内容追加到环境变量 LD_LIBRARY_PATH 的末尾:

/home/memory/mcr/MCR_R2018b/env/v95/runtime/glnxa64:/home/memory/mcr/MCR_R2018b/env/v95/bin/glnxa64:/home/memory/mcr/MCR_R2018b/env/v95/sys/os/glnxa64:/home/memory/mcr/MCR_R2018b/env/v95/extern/bin/glnxa64
1
2
export
/home/memory/mcr/MCR_R2018b/env/v95/runtime/glnxa64:/home/memory/mcr/MCR_R2018b/env/v95/bin/glnxa64:/home/memory/mcr/MCR_R2018b/env/v95/sys/os/glnxa64:/home/memory/mcr/MCR_R2018b/env/v95/extern/bin/glnxa64:$LD_LIBRARY_PATH:$LD_LIBRARY_PATH

这也不好测试,上传文件有点困难。

这儿是不是有点问题,不过现在看来能正常读到数据。

1
2
%分布式发电历史数据
P_DG_history=xlsread('DG-test.xlsx');

我得首先确定正常导入这张表,在麒麟V10完成测试,再尝试修改代码连接 33 数据库,如果可以的话,尝试在 33 服务器测试脚本成功。

来回倒腾这几个服务器。

部署步骤呢。部署 Matlab,安装 MCR,拷贝代码,运行。

需要写一份操作文档。

1
mcc -m data_DG_timingchaincloud.m -o data_DG_timingchaincloud -d /home/memory/matlab/Matlab_R2018b/projects/分布式发电资源可调节潜力评估/data_DG_timingchaincloud/
1
mcc -m case_DG_timingchaincloud.m -o case_DG_timingchaincloud -d /home/memory/matlab/Matlab_R2018b/projects/分布式发电资源可调节潜力评估/case_DG_timingchaincloud/

这个目录确实需要调整,整个程序都是靠BUG运行起来的。

1
2
%分布式发电历史数据
P_DG_history=xlsread('/home/memory/matlab/projects/data_DG_timingchaincloud/DG-test.xlsx');
1
2
3
% 在 data_DG_timingchaincloud.m 结尾保存数据
save('/home/memory/matlab/projects/data_DG_timingchaincloud/data_DG_timingchaincloud.mat', 'T', 'N_DG', 'H_DG', 'P_DG_min', 'P_DG_max', 'L_DG', 'P_DG_history');
toc;
1
2
%加载预编译的数据文件
load('/home/memory/matlab/projects/data_DG_timingchaincloud/data_DG_timingchaincloud.mat');
1
2
% 保存 P_DG_order 到 DG-test2.mat 文件
save('/home/memory/matlab/projects/case_DG_timingchaincloud/DG-test2.mat','E_phi_DG_p','En_DG_p','En_DG_p_random','H_DG','He_DG_p','L_DG','M_DG_p','N_DG','P_DG_history','P_DG_max','P_DG_min','P_DG_order','P_DG_pre','S_DG_p','T','i','j','m_DG_high','m_DG_low','m_DG_p_00','m_DG_p_01','m_DG_p_10','m_DG_p_11','p_DG_00','p_DG_01','p_DG_10','p_DG_11','r_0','r_1','sum_p_DG_p','z');
1
2
% 加载数据
load('/home/memory/matlab/projects/case_DG_timingchaincloud/DG-test2.mat');% 调用一维云模型算法的计算结果
1
2
3
4
5
6
writetable(P_pot_rup_DG, 'DG-result.xlsx', 'Sheet', '向上调频可调节潜力', 'WriteVariableNames', false);
writetable(P_pot_rdown_DG, 'DG-result.xlsx', 'Sheet', '向下调频可调节潜力', 'WriteVariableNames', false);
writetable(P_pot_sr_DG, 'DG-result.xlsx', 'Sheet', '旋转备用可调节潜力', 'WriteVariableNames', false);
writetable(P_pot_nsr_DG, 'DG-result.xlsx', 'Sheet', '非旋转备用可调节潜力', 'WriteVariableNames', false);
writetable(V_pot_frrup_DG, 'DG-result.xlsx', 'Sheet', '向上快速调整率可调节潜力', 'WriteVariableNames', false);
writetable(V_pot_frrdown_DG, 'DG-result.xlsx', 'Sheet', '向下快速调整率可调节潜力', 'WriteVariableNames', false);
1
2
% 连接 MySQL 数据库
javaaddpath("/home/memory/matlab/projects/DG_potential2/mysql-connector-java-8.0.28.jar");

重新打包一遍后,可以了。

在 33 服务器测试一遍 Demo 代码,或者直接开始写操作文档了。

操作文档写了三分之二,数据库连接测试完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
databasename = 'information_schema'
javaaddpath("/home/memory/matlab/Matlab_R2018b/dev/Matlab_R2018b/java/jar/toolbox/mysql/mysql-connector-java-8.0.28.jar")
conn = database(databasename,"root","tellhow1234!@#$",'Vendor','MySQL', ...
'Server','172.16.6.33','PortNumber',3306,'LoginTimeout',5);

disp(conn);

% 检查连接是否成功
if isopen(conn)
disp('数据库连接成功!');
else
error('数据库连接失败!');
end

selectquery = "SELECT * FROM INNODB_CMPMEM";
data = select(conn, selectquery);
disp(data);
1
2
3
4
5
6
7
8
9
数据库连接成功!
page_size buffer_pool_instance pages_used pages_free relocation_ops relocation_time
_________ ____________________ __________ __________ ______________ _______________

1024 0 0 0 0 0
2048 0 0 0 0 0
4096 0 0 0 0 0
8192 0 0 0 0 0
16384 0 0 0 0 0

下午了。

先打包最终的代码吧。

测试完成。

确实遇到点小问题,拉取代码就算了,推送代码都成了问题;在 33 服务器测试成功就有鬼了,连传输个文件都费劲。

我是要从数据库中解析生成一张 Excel 表格,再导入这张表执行算法么。

1
./run_data_DG_timingchaincloud.sh /home/memory/mcr/MCR_R2018b/env/v95
1
2
3
4
5
6
7
[root@sesp-test-1 data_DG_timingchaincloud]# ./run_data_DG_timingchaincloud.sh /home/memory/mcr/MCR_R2018b/env/v95
------------------------------------------
Setting up environment variables
---
LD_LIBRARY_PATH is .:/home/memory/mcr/MCR_R2018b/env/v95/runtime/glnxa64:/home/memory/mcr/MCR_R2018b/env/v95/bin/glnxa64:/home/memory/mcr/MCR_R2018b/env/v95/sys/os/glnxa64:/home/memory/mcr/MCR_R2018b/env/v95/sys/opengl/lib/glnxa64
Dynamic exception type: std::runtime_error
std::exception::what: Bundle#7 start failed: libXt.so.6: 无法打开共享对象文件: 没有那个文件或目录

出什么问题了这。

  1. 未安装 X11 工具包:系统未安装 libXt 库。
  2. MCR 依赖未满足:MATLAB Compiler Runtime (MCR) 需要 X11 库支持图形界面。
  3. 路径配置问题LD_LIBRARY_PATH 未包含 X11 库路径。
1
sudo yum install libXt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
安装  1 软件包

总下载量:173 k
安装大小:420 k
Is this ok [y/d/N]: y
Downloading packages:
libXt-1.1.5-3.el7.x86_64.rpm | 173 kB 00:00:00
Running transaction check
Running transaction test
Transaction test succeeded
Running transaction
正在安装 : libXt-1.1.5-3.el7.x86_64 1/1
验证中 : libXt-1.1.5-3.el7.x86_64 1/1
已安装:
libXt.x86_64 0:1.1.5-3.el7
1
find /usr -name "libXt.so.6"

执行完这一步,33 服务器竟然能成功运行脚本了。

废了挺长时间才终于把所有脚本文件上传至 33 服务器,尝试运行。

1
./run_all.sh /home/memory/mcr/MCR_R2018b/env/v95
1
2
[root@sesp-test-1 projects]# ./run_all.sh /home/memory/mcr/MCR_R2018b/env/v95
-bash: ./run_all.sh: /bin/bash^M: 坏的解释器: 没有那个文件或目录

这是换行符问题。

安装 dos2unix 工具,该工具用于将 Windows 格式的文本文件转换为 Unix/Linux 格式。

1
2
# CentOS/RHEL
sudo yum install dos2unix -y
1
dos2unix run_all.sh
1
2
3
4
[root@sesp-test-1 projects]# chmod +x ./data_DG_timingchaincloud/run_data_DG_timingchaincloud.sh
[root@sesp-test-1 projects]# chmod +x ./case_DG_timingchaincloud/run_case_DG_timingchaincloud.sh
[root@sesp-test-1 projects]# chmod +x ./DG_potential1/run_DG_potential1.sh
[root@sesp-test-1 projects]# chmod +x ./DG_potential2/run_DG_potential2.sh
1
2
3
4
[root@sesp-test-1 projects]# chmod +x ./data_DG_timingchaincloud/data_DG_timingchaincloud
[root@sesp-test-1 projects]# chmod +x ./case_DG_timingchaincloud/case_DG_timingchaincloud
[root@sesp-test-1 projects]# chmod +x ./DG_potential1/DG_potential1
[root@sesp-test-1 projects]# chmod +x ./DG_potential2/DG_potential2

特么的,靠转换还是不行,有的命令都乱码了,还是要在 Linux 环境下编辑脚本文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/bin/bash

echo "开始执行 /run_data_DG_timingchaincloud.sh"
./data_DG_timingchaincloud/run_data_DG_timingchaincloud.sh /home/memory/mcr/MCR_R2018b/env/v95
echo "/run_data_DG_timingchaincloud.sh 执行完成"

echo "开始执行 ./run_case_DG_timingchaincloud.sh"
./case_DG_timingchaincloud/run_case_DG_timingchaincloud.sh /home/memory/mcr/MCR_R2018b/env/v95
echo "./run_case_DG_timingchaincloud.sh 执行完成"

echo "开始执行 ./run_DG_potential1.sh"
./DG_potential1/run_DG_potential1.sh /home/memory/mcr/MCR_R2018b/env/v95
echo "./run_DG_potential1.sh 执行完成"

echo "开始执行 ./run_DG_potential2.sh"
./DG_potential2/run_DG_potential2.sh /home/memory/mcr/MCR_R2018b/env/v95
echo "./run_DG_potential2.sh 执行完成"

33服务器上脚本的 MCR 环境要更新下了。

1
./run_all.sh /home/memory/mcr/MCR_R2018b/env/v95

总算一步到位,执行成功。

我是要从数据库中解析生成一张 Excel 表格,再导入这张表执行算法么。

这张表的数据竟然有五万多行么。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
% 获取指定字段数据(假设字段名为 'load_value')
if isempty(data)
disp('未查询到数据!');
else
% 提取目标字段(替换为你需要的字段名)
target_field = 'load_value';
values = data.(target_field);

% 创建表格并导出Excel
result_table = table(values, 'VariableNames', {target_field});
excel_filename = 'output_data.xlsx';

% 写入Excel文件
try
writetable(result_table, excel_filename);
disp(['数据已成功导出至: ' excel_filename]);
catch ME
error('导出失败: %s', ME.message);
end
end

这样就成功导出了 Excel 表格,不过导出的是一列数据,需要完善下结果为一行数据。

一行放不下。。应该是根据日期来导出数据的吧,一个日期一行数据。

尝试了两个半小时,总算写出了最接近真相的一段代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
databasename = 'vpp_intranet'
javaaddpath("/home/memory/matlab/Matlab_R2018b/dev/Matlab_R2018b/java/jar/toolbox/mysql/mysql-connector-java-8.0.28.jar")
conn = database(databasename,"root","tellhow1234!@#$",'Vendor','MySQL', ...
'Server','172.16.6.33','PortNumber',3306,'LoginTimeout',5);

disp(conn);

% 检查连接是否成功
if isopen(conn)
disp('数据库连接成功!');
else
error('数据库连接失败!');
end

% selectquery = "SELECT * FROM t_vpp_rt_load";
% data = select(conn, selectquery);
% disp(data);

% 配置参数
excel_filename = 'daily_rows.xlsx';
date_column = 'data_date';
target_field = 'p';

% 获取所有日期列表
date_query = sprintf('SELECT DISTINCT DATE(%s) AS day FROM t_vpp_rt_load', date_column);
date_data = fetch(conn, date_query);
all_dates = date_data.day;
disp(all_dates);

% 初始化Excel文件(删除旧文件)
if exist(excel_filename, 'file')
delete(excel_filename);
end

% 逐日期处理
for i = 1:length(all_dates)
% 查询当日数据
current_date = all_dates{i};
day_str = datestr(current_date, 'yyyy-mm-dd');
query = sprintf('SELECT %s FROM t_vpp_rt_load WHERE DATE(%s) = ''%s''',...
target_field, date_column, day_str);
daily_data = fetch(conn, query);
disp(daily_data);

% 转置为行向量并创建表格
row_values = daily_data.(target_field)';
% 生成临时列名(例如 Var1, Var2...)
num_columns = length(row_values);
var_names = cellfun(@(x) sprintf('Var%d',x), num2cell(1:num_columns), 'UniformOutput', false);

% 创建表格(包含临时列名)
row_table = array2table(row_values, 'VariableNames', var_names);

% 计算写入范围
if i == 1
start_range = 'A1'; % 首次写入起始位置
else
start_range = sprintf('A%d', i); % 后续行依次递增
end

% 写入Excel(自动扩展行)
writetable(row_table, excel_filename,...
'WriteVariableNames', false,...
'Range', start_range); % 关键参数

fprintf('已写入 %s 到第%d行\n', day_str, i);
end

% 关闭连接
close(conn);
disp('全部数据导出完成!');

不过七十九天的数据,为什么只写了五十行。

哦不好意思算错了,就是五十天的数据。

校验了半天导出的数据,明确下数据库内的数据和导出到 Excel 表格的数据是否相同,但这里有一个明显的业务问题。

这个表的数据根据日期和厂站地区,查询后生成入参的 Excel 表格。

那这个表格应该长什么样子。

2025 年 2 月 26 日

1
git remote add origin http://dev.tellhowsoft.com/DefaultCollection/%E8%BF%88%E8%83%BD%E4%BA%92%E8%81%94Area/_git/guangxi-vpp-main
1
2
git push -u origin guangxi-vpp-dw
fatal: protocol 'http' is not supported

解决字符问题,还是无法推送代码,TFS 账号密码无效,登录失败,推送代码也失败。

修改下昨天的代码,根据不同厂站分别导出各自的 Excel 表格。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
databasename = 'vpp_intranet';
javaaddpath("E:\Matlab\java\jar\toolbox\mysql\mysql-connector-java-8.0.28.jar");
% javaaddpath("/home/memory/matlab/Matlab_R2018b/dev/Matlab_R2018b/java/jar/toolbox/mysql/mysql-connector-java-8.0.28.jar");
conn = database(databasename,"root","tellhow1234!@#$",'Vendor','MySQL', ...
'Server','172.16.6.33','PortNumber',3306,'LoginTimeout',5);

disp(conn);

% 检查连接是否成功
if isopen(conn)
disp('数据库连接成功!');
else
error('数据库连接失败!');
end

% 配置参数
date_column = 'data_date';
target_field = 'p';

% 获取所有厂站列表
city_query = 'SELECT DISTINCT city_no FROM t_vpp_rt_load';
city_data = fetch(conn, city_query);
all_cities = city_data.city_no;
disp(all_cities);

% 逐厂站处理
for city_idx = 1:length(all_cities)
current_city = all_cities{city_idx};
excel_filename = sprintf('daily_rows_city_%s.xlsx', current_city); % 根据厂站生成文件名

% 初始化Excel文件(删除旧文件)
if exist(excel_filename, 'file')
delete(excel_filename);
end

% 获取当前厂站的所有日期列表
date_query = sprintf('SELECT DISTINCT DATE(%s) AS day FROM t_vpp_rt_load WHERE city_no = ''%s''', date_column, current_city);
date_data = fetch(conn, date_query);
all_dates = date_data.day;
disp(all_dates);

% 逐日期处理
for date_idx = 1:length(all_dates)
% 查询当日数据
current_date = all_dates{date_idx};
day_str = datestr(current_date, 'yyyy-mm-dd');
query = sprintf('SELECT %s FROM t_vpp_rt_load WHERE DATE(%s) = ''%s'' AND city_no = ''%s''',...
target_field, date_column, day_str, current_city);
daily_data = fetch(conn, query);
disp(daily_data);

% 转置为行向量并创建表格
row_values = daily_data.(target_field)';
% 生成临时列名(例如 Var1, Var2...)
num_columns = length(row_values);
var_names = cellfun(@(x) sprintf('Var%d',x), num2cell(1:num_columns), 'UniformOutput', false);

% 创建表格(包含临时列名)
row_table = array2table(row_values, 'VariableNames', var_names);

% 计算写入范围
if date_idx == 1
start_range = 'A1'; % 首次写入起始位置
else
start_range = sprintf('A%d', date_idx); % 后续行依次递增
end

% 写入Excel(自动扩展行)
writetable(row_table, excel_filename,...
'WriteVariableNames', false,...
'Range', start_range); % 关键参数

fprintf('厂站 %s:已写入 %s 到第%d行\n', current_city, day_str, date_idx);
end

% 调整Excel表格列宽
try
% 调用Excel COM接口
excel = actxserver('Excel.Application'); % 启动Excel
excel.Visible = false; % 不显示Excel界面
workbook = excel.Workbooks.Open(fullfile(pwd, excel_filename)); % 打开文件
sheet = workbook.Sheets.Item(1); % 获取第一个工作表

% 设置列宽(单位:字符宽度)
sheet.Columns.ColumnWidth = 15; % 统一设置为15字符宽度

% 保存并关闭
workbook.Save();
workbook.Close();
excel.Quit();
delete(excel); % 释放COM对象
fprintf('厂站 %s:Excel表格列宽已统一调整\n', current_city);
catch ME
warning('调整Excel列宽失败: %s', ME.message);
end

fprintf('厂站 %s 数据导出完成!\n', current_city);
end

% 关闭连接
close(conn);
disp('全部厂站数据导出完成!');

0401厂站从12月27号算起,到2月13号,49天,导出表格数据四十九行。

0402厂站从12月26号算起,到2月13号,50天,导出表格数据五十行。

导出表格数据无误,接下来测试使用新数据,算法能否正确进行。

1
./run_all.sh /home/memory/mcr/MCR_R2018b/env/v95
1
./run_all.sh /home/memory/mcr/MCR/v95

没成功。

1
2
错误使用 save
无法写入文件 /home/memory/matlab/projects/data_DG_timingchaincloud/data_DG_timingchaincloud.mat: 权限被拒绝。

我导出的这两张表格不规范,算法调用失败,具体怎么个规范法,我也不清楚。

我不想在这里呆下去了。

什么都不告诉我,没有同这群家伙共进退的感觉。

工作上的事情,甚至有关于仕途前程的事都不给我讲,我要怎样才能信得过这群家伙。

还有多长时间,还有多少机会。

一概不知。

2025 年 2 月 27 日

代码,我上传了。

文档,我写好了。

Xshell

2025 年 2 月 18 日

Xshell——安装使用教程(图文详解)_xshell安装教程-CSDN博客

这是官方提供的 Xshell,Xftp 免费版下载地址:

家庭/学校免费 - NetSarang Website (xshell.com)

这是官网正版全家桶下载地址:

所有下载 - NetSarang Website (xshell.com)

image-20250218093514200

Thank You - Free User Registration Success - NetSarang Website (xshell.com)

xshell、xftp、Xmanager绿色破解版下载地址(持续更新) - 翎野君 - 博客园 (cnblogs.com)

Xmanager 6 安装成功了,看看 Xshell 远程连接开启 X11 服务后能不能直接安装 Matlab。

image-20250218095614810

安装正在进行中,目前来看一切顺利。

1
[root@thmn Matlab_R2018b]# sudo cp '/home/memory/matlab/Matlab_R2018b/Crack/license_standalone.lic' '/home/memory/matlab/Matlab_R2018b/dev/Matlab_R2018b/licenses'
1
[root@thmn Matlab_R2018b]# sudo cp '/home/memory/matlab/Matlab_R2018b/Crack/bin/glnxa64/matlab_startup_plugins/lmgrimpl/libmwlmgrimpl.so' '/home/memory/matlab/Matlab_R2018b/dev/Matlab_R2018b/bin/glnxa64/matlab_startup_plugins/lmgrimpl'
1
cd /bin
1
./matlab

Matlab 已经安装成功,尝试编译下工程文件。

在 FinalShell 里把工程文件转发到 Linux 服务器上,在 Matlab 上编译。

1
mcc -m doubleInput.m -o doubleInput

报错了。

1
2
3
4
>> mcc -m doubleInput.m -o doubleInput.m
Illegal output name: 'doubleInput.m'
(standalone and shared library target output names must begin with a letter or '_' and contain
only alpha-numeric characters or '_')

搞错了,这样打包,打包成功。

1
mcc -m doubleInput.m -o doubleInput

写几个测试用例。

1
disp("Hello, World");
1
2
3
4
(base) [root@thmn test]# ./run_doubleInput.sh 
------------------------------------------
Usage:
./run_doubleInput.sh <deployedMCRroot> args

当你看到这样的错误信息时,它表明你尝试运行的 doubleInput 可执行文件(尽管你是通过 run_doubleInput.sh 脚本调用的)实际上是一个依赖于 MATLAB Compiler Runtime (MCR) 的程序。这个错误信息提示你需要指定 MCR 的安装根目录作为程序的第一个参数。

在安装路径中获取MCR。

进入 matlab 输入 mcrinstaller,会弹出 mcr 的路径。

1
mcrinstaller

16 服务器上还没有成功配置 MCR,不过也不要紧,编译好的脚本将来能在麒麟上成功运行就行了。

他妈的麒麟虚拟机打不开了。。一直黑屏。

为什么,艹。

下午再试试吧,上午最后一步测试环节竟然被卡掉了。

下午咯。

今天这虚拟机是犯病了还是怎么,为什么开机黑屏呢。

受不了了,为什么问题会出在这里。。

银河麒麟桌面操作系统V10登录后黑屏_linux系统管理工具-CSDN专栏

重装麒麟虚拟机,安装运行环境,测试成功。

今天的任务完成了。

CEMEB

2025 年 3 月 5 日

完善的帮助文档,专属的售后技术,更有视频教程、官方论坛反馈,坚持服务至上。

官网:高品质开源商城系统-CRMEB官网

文档中心:CRMEB文档

技术社区:CRMEB技术社区|开源商城系统开发者社区-CRMEB社区

应用市场:CRMEB应用市场,汇集全品类互联网软件行业解决方案!多、快、好、省

官方商城:CRMEB应用市场

开源地址:众邦科技: CRMEB赋能开发者,助力企业发展 (gitee.com)

结局

2025 年 3 月 17 日

有关那里的一切,到这里就结束了,也是时候真正同过去说一声再见了。

我好像忘记了生活原本的模样。

转眼间就来到了离开公司的第二周,就在今天上午十一点半,最后一项工作任务交接完成,我与这家单位之间便再无瓜葛。

要说有的话,也许是下个月中旬还能享受到最后一周工作的薪资发放吧。

周五那天下午离开工位的时候,我没有一点感觉。

收拾书包,接杯水,出门,坐电梯,下楼,走出大门,过街,穿过公园,沿着人行道,就这么慢慢地往回走。

那天中午点了在那里的最后一份外卖,最后一次提着餐盒去负一层的餐厅吃饭,最后一次乘着电梯上楼去。

三个月前,我离开了故乡跋山涉水选择来到这里,只为能在有限的时间里在异乡为自己谋条出路,为自己的将来以及后路考虑周全。

为了做到这一切,我早已规划好了未来可能的生活蓝图。

可惜天不随人愿,命运并没能让我得偿所愿。

命运仿佛同我开了一个巨大的玩笑,我的整个职业生涯在两周不到的时间里被击溃,仅剩我本人仍旧矗立在这诺大城市中央的狭小一方。

尽管内心深处早已满目疮痍,然而即便是再大的暴风雨也休想阻挡我追寻梦想的脚步。

不过些许风霜罢了。

就像巩老师今下午对我说的,这也许是我命中必遭的一难。

其实哪里有那么多玄学,说出来连我自己都觉着可笑,我分明不过在感受生活的风雨拍打着自己的脸颊而已。

下午抽时间穿戴好下楼出门,去到最近的生鲜店里买了蔬菜、挂面和饺子,回来的路上猛然惊觉今天竟然是久违的艳阳高照日。

清风微拂,杨柳顿挫,树影斑驳。

尽管眼前街道上是飞驰而过的车流和嘈杂且此起彼伏的鸣笛声,然而在大自然独到又恰到好处的光景映衬下,眼前简直是一副沁人心脾的美丽画卷。

今天真是个好天气。

今天也同样有个好心情。

南京

南京

2024 年 12 月 19 日

最近生活中确实有些力不从心,记录生活的好习惯可不能丢掉了。

当然工作上暂时还没有什么烦心事,实习第一周当然不会那么紧张,只是我还需要一点时间适应这样的生活节奏。

刚才翻过个人博客,看到了去年冬天到今年早些时候的一些生活笔记和心情日记,不免感慨万千。

时间过得真快。

今天早上是这周起床最晚的一天,七点半还赖在被窝里不肯爬起来,早饭也没有煮两筷子挂面,啃了两口饼干就着一口水就算早饭了。

看来懒惰还是在向前追赶着我。

昨天晚上睡前又跟前天晚上一样,抽时间学习了一小会儿,为第二天能顺利完成工作任务做做准备。

下班前又去找好伙伴聊会儿天,南京这块儿地别的不说,物价倒挺高,也不枉是省会城市,出门找个饭店买碗面都得十八块起步。

还是自己家里做饭吃实惠,反正蔬菜瓜果在哪里都不算贵。

早上一般都在七点钟前醒来,煮一碗热气腾腾的挂面,加个鸡蛋更能饱腹,背起书包起身下楼就去上班了。

中午基本都在点外卖,十一点半那会儿就能在工位上直接快速解决掉,一点钟前还能抽出半个多小时睡个觉。

下午又是忙碌又充实的时光,自己搁那鼓捣几个小时,到了五点半这会儿天就黑了。

下班后赶紧出门下楼签到打卡,紧赶慢赶坐上公交十分钟到百家湖小区这边,走路回家还能顺便买点好吃的好喝的。

只是最近上班没时间了,上周周二出门买棉被,周三周四周五隔壁小区取快递,周日好歹再出门买了蔬菜和肉丸子,今天刚好吃完。

挺长时间没有好好游赏这个城市了,上周刚来的那几天本来是最无忧无虑的时候,奈何在家呆久了实在是冻手冻脚,索性白天在家基本钻被窝里。

被窝里也很冷啊,得亏周二晚上老妈给买了枕头,这几天能盖着两床被子睡觉了,钻被窝里暖和了整个屋里也好像热乎起来了。

今天早上坐在工位上,又是第一个到的,想想还有今天过后还有一天就盼到周末就开心。

上班坐在工位上感觉像在坐牢,下班后又是刑满释放的感觉。

最喜欢下班后等公交的时间,我可以抽出时间来观赏南京城的夜景,坐公交到达目的地后走路回家的那段时间,也是非常享受的。

我喜欢南京这个城市,尽管目前为止还没能好好出去逛一逛。

也许年后,明年春暖花开气温逐渐上升以后,我才有兴致出门好好在这个城市里溜达溜达,毕竟现在大冬天实在不想出门。

到了那个时候,我也许会考虑单独为南京开一篇新的栏目。

最近有些忙碌,确实晚上回来就想着刷刷,今天就抽空来记录下生活,写得有些琐碎。

希望明天一切顺利。

提前预祝,周末愉快。

体检

2025 年 1 月 4 日

今天是周末嘛,怎么醒得格外早,七点二十分的闹钟。

体检去,完了顺便理个发。

洗把脸,穿戴好衣服,八点多出门下楼,到街对面打个车,十块钱。

站在路边的时候有个哥们从我身后经过,提醒我身份证掉地上了,真是个大好人。

十分钟不到就到了美年大健康江宁分院,刚进大门就跑保安室问路了,哥们跟我说体检的话出门右拐,就在隔壁。

进了医院,到前台办理体检证明,出示下身份证,拍张照片,就领到自己的体检单据了。

先去一楼最东边做肝功能检测,签了字填了单子,脖子上挂个圈套啥的,进门后带上帽子前胸双肩贴进仪器,十秒钟不到就结束了。

剩下的检查事项都在二楼了。

有啥不懂的流程直接找附近的工作人员问就可以了,人美心善,看到有人走过来也会主动问你需不需要帮助。

上了二楼,在工作人员的指导下,先抽血,我来得比较早这边基本没人。

涂了碘伏,贴上消毒贴,在大厅沙发上坐一会儿,止血后再起身去别的科室。

内科,外科,内眼科,外眼科,一般性检查,心电图,尿检,很快就结束了。

最后一项竟然是吃早餐,鸡蛋,面包,包子,馒头,牛奶,果汁,完全免费不要钱,简直太人性化了。

拿了单子去一楼前台,交给服务人员后,今天的体检项目就结束了。

九点半。

出门,街边打个车子,十分钟到了百家湖国际花园门前,十二块钱。

去理个发,过两条街,还是三周前来看过的那家店,果然跟我想的一样,二十块钱一位,值了。

理完发出门右拐到生鲜市场去,买点火锅丸子,十七块钱。

十点多回家咯,好兄弟这会儿也理完发吃完饭了,来两盘。

玩到十一点多,四场,今晚就不打了,从今天开始尽量不熬夜,改掉熬夜的坏毛病。

回来的时候取了快递,十个墙贴钩子,我用掉了四个。

十二点多钻进被窝,简直太温馨了。

今天早上十点多那会儿理完发,出门买了火锅丸子走在回家的那段路上,我感觉自己就是这个世界上最幸福的人。

下午两点半,躺下来睡一小会儿。

2025 年 1 月 6 日

前天,周六体检完毕,今天早上出结果。

体检报告已出。

肝功三项检查什么-医疗科普-百度健康 (baidu.com)

CT和DR检查有什么区别不同-医疗科普-百度健康 (baidu.com)

1
2
3
4
5
拍胸部DR和CT是两种不同的医学影像检查方法,它们在成像原理、图像分辨率、辐射剂量、适用范围以及检查时间等方面都存在显著的区别。以下是关于这两种检查方法的详细介绍以及它们各自英文名词的含义:

一、英文名词含义
DR:全称是Digital Radiography,即数字放射成像。它是一种利用X射线穿透人体组织后形成的影像进行诊断的医学影像技术。
CT:全称是Computed Tomography,即计算机断层扫描。它是一种利用X射线通过多个角度扫描人体不同部位,结合计算机技术生成横断面图像进行诊断的医学影像检查方法。

添加客服,申请开具电子发票。

肝功能三项,血型,这两项额外花费共计36块。191。

image-20250106111611239

image-20250106111706091

1
2
3
4
5
6
7
税号和社会信用组织代码是一回事吗
税号和社会信用代码确实是一回事。在我国实施三证合一制度后,税号与统一社会信用代码已经实现了统一。

一、税号与统一社会信用代码的统一

三证合一制度的实施:我国实施的三证合一制度,即将企业依次申请的工商营业执照、组织机构代码证和税务登记证三个证件合为一个证件,目的是提高市场准入效率。“一照一码”则是在此基础上的进一步改革,通过将一个统一的社会信用代码作为企业唯一的识别码,实现了企业信息的整合与共享。
税号与统一社会信用代码的关系:在三证合一制度下,企业的税号实际上就是新的工商营业执照上所记载的统一社会信用代码。这个代码具有唯一性,确保了企业的准确识别。

申请开具发票,登记个人/公司发票信息。

image-20250106110611090

image-20250106110728166

提交完成。

对数视力表和国际标准视力表对照表 - 百度文库 (baidu.com)

发票开具完成了吗,早上快十一点提交的,下午两点半就结束了。

软著申请

2025 年 1 月 6 日

我看看那个第二成绩单系统。

山西大学信息门户网站:http://nehall.sxu.edu.cn/#/newsOld。

image-20250106090843938

image-20250106091008207

image-20250106091442816

软件著作权申请教程(超详细)(2024新版)软著申请-CSDN博客

💻软著申请全攻略 (baidu.com)

保姆级软著申请全过程(计算机软件著作版权申请)-腾讯云开发者社区-腾讯云 (tencent.com)

中国版权保护中心 (ccopyright.com.cn)

image-20250106092612502

image-20250106092948544

艹,实名认证还要手持身份证照片。。

晚上回去再看看。

软件著作权如何申请?个人申请软著需要提供哪些材料? (baidu.com)

要是三两个月前提前想到这一步的话,没准到现在软著就能成功申请下来了,目前从零开始申请流程的话有点赶紧,不太现实。

2025 年 2 月 21 日

如何下载软件著作权电子版 (baidu.com)

(29 封私信 / 31 条消息) 在淘宝上购买软件著作权被学校查到怎么办? - 知乎 (zhihu.com)

社会实践

2025 年 1 月 6 日

三下乡返家乡官网 (youth.cn)

image-20250106091442816

学校方面完全避免问题。

这个大学生三下乡社会实践网站登录不进去,跟学校第二成绩单系统激活账号什么的有关系,总之目前解决不了社会实践凑学分的问题。

证书

2025 年 1 月 7 日

📈三级网络安全管理员的职业前景与优势 (baidu.com)

刚群聊里通知只要在这个网站下查到的证书,都是有效的。

1
关于计算机等级考试的问题,其他学生用的一个网站,zscx.osta.org.cn,这个网站能查询到就算。有同学查询了把结果发上来一下。

技能人才评价证书全国联网查询 (osta.org.cn)`

image-20250107162021261

高级搜索 网络–职业技能鉴定网 (zgks.net),这个鸟网站查不到。

中国教育考试网 (neea.edu.cn)

image-20250107162420217

中国计算机技术职业资格网 (ruankao.org.cn)

image-20250107162756892

中国人事考试网 (cpta.com.cn)

image-20250107163048143

《网络与信息安全管理员》2024国家职业技能等级认证(职业方向:网络安全管理员、信息安全管理员、数据安全管理员) (qq.com)

网络与信息安全管理员 (数据安全管理员)职业技能等级证书–JYPC证书网 (xyfsw.net)N

2025 年 1 月 8 日

山西大学大学生第二成绩单管理系统 (sxu.edu.cn)

购票

2025 年 1 月 9 日

这么快一月份就到九号了。

成人票转学生票?误区 (baidu.com)


漫漫征途:筑梦于职场风雨,吟咏于日常诗意
https://test.atomgit.net/blog/2024/12/16/漫漫征途:筑梦于职场风雨,吟咏于日常诗意/
作者
Memory
发布于
2024年12月16日
更新于
2025年2月26日
许可协议