本文最后更新于:1 个月前
期盼
2024 年 12 月 21 日
冬至日。
忙碌的工作日已经,迎来工作生活后第一个周末的清晨。
对这份工作也不会抱有多大期待,只希望在最忙碌的时间里也不要忘记好好犒劳自己,在最难过的日子里也要能咬咬牙坚持下来。
昨天是这周的最后一个工作日,原本计划昨天上午完成这个栏目的前言,但奈何早上也没能抽出时间静下心来写点东西。
早上还看见高中那会儿的班级新建了一个班群,说是后续会有新的通知请及时关注,又是收集已毕业学子的那套把戏,不过没多少人填。
周四晚上情不自禁就把自己的荣誉和能力也统统写上去,还怂恿好兄弟也把自己精彩的校园经历整了上去。
这么看起来还算不错。
前天晚上刚吃完饭就跟爸妈视频聊了一会儿,昨天下班回家路上接到大哥的视频电话,取完快递回家后又跟正在备考期末的好兄弟语音两小时。
我的家人,永远是我最坚强的后盾,一想到即使远在他乡也有远方的人时刻挂念着自己,我便无所畏惧。
一介平凡人,尽力了便好。
下午太阳出来以后出门走走,冬至日里也要让自己吃上热乎的饺子。
努力工作,快乐生活。
泰豪软件 苇湖梁
2024 年 12 月 17 日
昨天下午一直在看核心代码,也没看出个所以然来,没有申请下账号密码不能登录 fts 代码仓,只好直接解压开心给的代码压缩包。
iois-whl-svr - Repos (tellhowsoft.com)
今早用薛松的账号登录 fts 代码仓,尝试拉取代码。
一直报错:
组长,石彤彤,王开心,康春辉帮忙看问题所在,搞了一个多小时。
康春辉帮忙给发了这么个配置文件,放在本地公钥所在目录下,就能正常使用 SSH 地址的方式拉取代码到本地。
配置文件内容:
1 2 3 4 Host dev.tellhowsoft .com HostName dev.tellhowsoft .com HostKeyAlgorithms +ssh-rsa PubkeyAcceptedKeyTypes +ssh-rsa
拉取完成,新增分支,IDEA切换分支,就可以专心搞开发了。
扒一扒Nacos、OpenFeign、Ribbon、loadbalancer组件协调工作的原理大家好,我是三友~~ 前几 - 掘金 (juejin.cn)
接口文档
2024 年 12 月 19 日
所有模块接口的文档都拿到了,网页均保存至本地集锦。
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 <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(); } }
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 失败了。
一篇文章带你搞定 SpringBoot 整合 Swagger2_Java 开发学习最全合集-CSDN专栏
SpringBoot集成Swagger 2 - luorx - 博客园 (cnblogs.com)
解决springboot接入springfox-swagger2遇到的一些问题_java_脚本之家 (jb51.net)
就这个文档对症下药了,果然是 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; @ApiOperation(value = "License 证书上传", notes = "License 证书上传") @PostMapping("/upload") public CommonResult<Boolean> upload (MultipartFile file) { ........................................ } ...................................... }
如果出现报错就按照上面提到的方案,配置文件里增加配置即可一步到位解决,现在接口文档总算可正常查看了。
1 spring.mvc.pathmatch.matching-strategy =ANT_PATH_MATCHER
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博客
实战:Nacos配置中心的Pull原理,附源码在单体服务时代,关于配置信息,管理一套配置文件即可。 而拆分成微服务之后, - 掘金 (juejin.cn)
SpringCloud之Nacos2.X作为配置中心_nacos配置数据库-CSDN博客
1 2 3 4 配置中心的信息一般都是放在bootstrap.yml 中; 初始化的时候,Bootstrap Context 负责从外部源加载配置属性并解析配置;Bootstrap属性有高优先级,默认情况下,它们不会被本地配置覆盖; 然后再读取application.yml中的配置,进行配置合并,完成项目的启动。
(九)漫谈分布式之微服务组件篇:探索分布式环境下各核心组件的必要性!记得几年前团队在招聘时,要求候选人有分布式/微服务项 - 掘金 (juejin.cn)
一如既往的高质量佳作。
特么一百一十五张表。。
两点多看到三点半多,这代码看多了头昏脑胀的,耍会儿摸鱼网站。
软件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博客
大部分原因是本地的host文件中没有配置正确的主机名和对应的 IP 地址,要与远程服务器的一致才能够运行成功
一般都是在C:\Windows\System32\drivers\etc地址下的host文件中添加或修改自己项目对应的主机名和IP地址 例如:xxx.xxx.xxx.xxx:主机名
1 2 3 # This line is auto added by aTrustAgent, do not modify, or aTrustAgent may unable to work127.0.0.1 localhost.sangfor.com.cn192.168.118.118 nacos.tellhow.com
那远程数据库连接失败的问题也就能解决了,同理。
1 2 3 4 # This line is auto added by aTrustAgent, do not modify, or aTrustAgent may unable to work127.0.0.1 localhost.sangfor.com.cn192.168.118.118 nacos.tellhow.com192.168.118.118 mysql.tellhow.com
就这么折腾一番,项目果然能在本地跑起来了,测试日志也顺利打印。
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-test</artifactId > </dependency >
测试下 Nacos 提供的示例代码,打印配置:
这个测试暂时还跑不起来,先尝试本地启动所有模块吧。
1 2 3 4 5 6 # This line is auto added by aTrustAgent, do not modify, or aTrustAgent may unable to work127.0.0.1 localhost.sangfor.com.cn192.168.118.118 nacos.tellhow.com192.168.118.118 mysql.tellhow.com192.168.118.118 rabbitmq.tellhow.com192.168.118.118 redis.tellhow.com
IDEA控制台不同类型日志显示不同颜色_idea设置控制台输出日志颜色-CSDN博客
扒一扒Nacos、OpenFeign、Ribbon、loadbalancer组件协调工作的原理大家好,我是三友~~ 前几 - 掘金 (juejin.cn)
License 基础学习
2024 年 12 月 18 日
昨天申请的内部账号使用权限已经按开通了,看见流程已走完,但账号密码没有。
我真的了解泛型吗?前言 曾经年少轻狂的我,以为自己已经轻松拿捏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)
生成公钥私钥 :
生成 license 文件:
新建工程,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 官网,直接下载这个就行:
license-common 模块打包成功:
原来这个@Value注解搞错了,导入的是 lombok 的 @Value 注解,怪不得读取不到配置文件:
1 2 3 4 5 spring.application.name=license-server license.licensePath=/Users/cxt/Downloads/license/license.lic license.privateKeysStorePath=/springboot-license/license-server/license/zuiyuPrivateKeys.keystore
在您的代码中,出现“找不到符号”错误的原因是因为您混淆了Spring的@Value
注解和Lombok的@Value
注解。
Spring的@Value
注解 :用于将配置文件(如application.properties
或application.yml
)中的值注入到Spring管理的bean的字段中。这是您想要使用的注解,以便从配置文件中读取licensePath
和privateKeysStorePath
的值。
Lombok的@Value
注解 :这是Lombok库提供的一个注解,用于自动为类的字段生成getter方法(以及可能的setter和equals
/hashCode
/toString
方法),但它并不用于注入配置值。
从您的代码和描述来看,您想要使用Spring的@Value
注解来注入配置值,但是可能由于以下原因之一导致编译错误:
特么起了项目也请求不到接口,我得再调试一番。
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 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" : "" } }
证书生成成功。
1 2 3 4 5 spring.application.name =license-server license.licensePath =C:/Users/Lenovo/Downloads/license/license.lic license.privateKeysStorePath =/license-server/license/zuiyuPrivateKeys.keystore
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 to { [/license/generateLicense], produces [application/json;charset=UTF-8]}: There is already 'licenseCreatorController' bean method com.memory.licenseserver.controller.LicenseCreatorController
初始化证书生成参数,日期字符串转换日期:
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 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
今下午,就这两个文档,帮了大忙:
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:
编译模块出现这样的问题,是 JDK 版本和 Maven 版本不兼容导致的,JDK 降成 8,看来 Maven需要再用 3.6.1 了
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 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.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
先提交推送这部分代码吧,这一天也算没白干。
捋一捋这个项目的生效思路:
其实一下午更多的时间里都在搞环境不兼容问题,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
那就支持获取本机硬件信息:
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 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 private Boolean checkIp = true ; private List<String> ipAddress; private Boolean checkMac = true ; private List<String> macAddress; private Boolean checkCpu = true ; private String cpuSerial; private Boolean checkMainBoard = true ; private String mainBoardSerial;
服务端优化
2024 年 12 月 20 日
今天早上继续完善这个功能,阳哥找我聊了下开发进度,我基本捋清楚了这个项目的业务流程,果然着眼于用户需求从客户的角度出发,才能解决问题。
用技术实现功能,解决实际问题。
Memory/license-server (gitee.com)
1 2 3 4 5 6 <dependency > <groupId > de.schlichtherle.truelicense</groupId > <artifactId > truelicense-core</artifactId > <version > 1.33</version > </dependency >
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) { 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 @PostMapping(value = "/generateLicense") public BaseResponse<Boolean> generateLicense (@RequestBody LicenseCreatorParamDTO param) { ThrowUtils.throwIf(ObjectUtils.isEmpty(param), ErrorCode.PARAMS_ERROR, "生成证书所需参数不能为空" ); boolean result = licenseCreateService.generateLicense(param); return ResultUtils.success(result); }
service 层:
1 2 3 4 5 6 7 8 9 10 11 12 13 public boolean generateLicense (LicenseCreatorParamDTO param) { this .checkLicense(param); this .setParam(param); return this .generate(); }
找不着生成路径:
1 2 3 4 5 6 7 8 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
这又找不着生成目录了:
1 System . getProperty("user.dir" )
特么 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
下午三点四十六分,基本完成这个模块的编写了,测试也圆满完成,稍作休息,开启下一步。
2024 年 12 月 23 日
上周实现的服务端生成证书参数还有些问题。
2024 年 12 月 14 日
保存证书信息:
证书名,所属项目,IP,MAC,许可期限,申请人,证书状态。
证书生成,每个证书生成成功后都支持下载,证书的命名需要同绑定的项目模块相映射,服务端可保存多个有效证书。
客户端需更新下上传证书校验逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public LicenseContent generateLicense (LicenseCreatorParamDTO param) { Boolean checkResult = this .checkLicense(param); ThrowUtils.throwIf(!checkResult, ErrorCode.OPERATION_ERROR, "证书生成参数校验失败" ); Boolean setResult = this .setParam(param); ThrowUtils.throwIf(!setResult, ErrorCode.OPERATION_ERROR, "证书生成参数设置失败" ); 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 public LicenseContent generateLicense (LicenseCreatorParamDTO param) { Boolean validated = this .validateAndSetParam(param); ThrowUtils.throwIf(!validated, ErrorCode.PARAMS_ERROR, "证书生成参数校验失败" ); 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 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 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, "证书信息保存失败" );
今天早些时候的接口文档派上用场了,根据请求示例构造生成证书请求:
到目前为止其实能看到证书生成的全过程没有问题,本次新增了客户端硬件参数并优化了校验参数逻辑,这里问题出在持久化至数据库。
很明显是由于 ipAddress
及macAddress
为 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 private String ipAddress;private String macAddress;
1 2 3 4 5 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 }
生成证书不需要传参主题 subject,本地配置写死即可:
1 2 license.subject =zuiyu_demo
证书生成还应该优化下,签名文件和验签文件,应该如何生成,可以在证书生成过程中加载配置生成。
生成证书需根据专门设置证书名称存放,同名证书会被覆盖掉。
使用 UUID 作为证书名,从数据库中下载指定证书时应该传入证书序号以针对性下载,这都是很简单的业务逻辑。
获取证书列表初步完成,分页获取已生成的证书列表。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @ApiOperation(value = "获取 License 证书列表", notes = "获取 License 证书列表") @PostMapping(value = "/getLicenseInfoList") public BaseResponse<Page<LicenseInfo>> getLicenseInfoList (@RequestBody LicenseInfoQueryRequest request) { ThrowUtils.throwIf(ObjectUtils.isEmpty(request), ErrorCode.PARAMS_ERROR, "生成证书所需参数不能为空" ); Page<LicenseInfo> licenseInfoList = licenseInfoService.getLicenseInfoList(request); return ResultUtils.success(licenseInfoList); }
1 2 3 4 { "currrent" : "1" , "size" : "10" }
2024 年 12 月 26 日
文档,请求参数,响应参数
证书下载,证书名。
响应参数:
1 2 3 4 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());
1 2 3 4 5 @TableId(type = IdType.ASSIGN_UUID) private String id;
分模块后有个问题,本地工作目录又多了一层,但将来部署的时候是单个项目部署,路径可能不一致,不过考虑到打包是统一打包的,应该不会有路径问题。
我试试。
没有问题了。
1 2 3 4 5 6 7 8 9 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 @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 downloadFileName = "license.lic" ; try { BufferedInputStream inputStream = new BufferedInputStream (Files.newInputStream(Paths.get(filePath))); BufferedOutputStream outputStream = new BufferedOutputStream (response.getOutputStream()); response.setContentType("application/octet-stream" ); 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();
先生成四个证书,看起来很完善的,证书均生成成功,证书信息也成功保存至数据库中。
接下来就是下载证书了。
搞错了,应该是 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();
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: Content-Type: application/json{ "licenseId" : "2729dbda-513d-4f63-b975-2c4a01d49522" } <> license.lic
证书生成,校验用户状态;证书下载,校验用户状态。
1 2 3 4 5 UserInfo loginUser = userInfoService.getLoginUser(request); ThrowUtils.throwIf(ObjectUtils.isEmpty(loginUser), ErrorCode.NOT_LOGIN_ERROR, "用户未登录" ); 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, "指定证书不存在" );UserInfo loginUser = userInfoService.getLoginUser(request); ThrowUtils.throwIf(ObjectUtils.isEmpty(loginUser), ErrorCode.NOT_LOGIN_ERROR, "用户未登录, 无法下载证书" ); 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 @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, "账号重复" ); } 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 @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 String text = UserRoleEnum.ADMIN.getValue();String role = loginUser.getUserRole();
取错值了,getText 是“管理员”。
客户端优化
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 @PostMapping("/upload") public CommonResult<Boolean> upload (MultipartFile file) { Boolean result = licenseVerifyService.upload(file); return CommonResult.success(result); }
1 2 3 4 5 6 7 8 9 spring.application.name =license-client server.port =8082 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 @Override public Boolean upload (MultipartFile file) { 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); } return false ; }
好极了,上传文件到客户端目录下已经测试完成:
证书安装:
1 2 3 4 5 6 7 8 9 10 11 12 13 @PostMapping("/install") public CommonResult<LicenseContent> install () { LicenseContent result = licenseVerifyService.install(); return CommonResult.success(result); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @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); }
这就安装成功了,看来上周做的工作很到位,到这一步为止基本没有什么问题。
证书验证。
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" ))); } }
证书校验定时任务执行,出问题了:
1 2 3 4 5 6 7 8 9 10 @Resource private LicenseVerifyService licenseVerifyService;@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 public boolean verify () { LicenseManager licenseManager = LicenseManagerHolder.getInstance(null ); DateFormat format = new SimpleDateFormat ("yyyy-MM-dd HH:mm:ss" ); 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 ; } }
怪了。
再跑一遍上周的 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 @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" ); } @Scheduled(cron = "*/2 * * * * *") public void getArticleContent () { System.out.println("定时任务执行" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss" ))); }
应该在项目启动后,首先安装临时证书,才能保证定时执行证书校验任务的正常运行,这样的业务流程也合情合理。
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 () { licenseVerifyService.verify(); }
很显然运行成功了:
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: 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: 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 日
@PostConstruct注解 @PostConstruct
注解可以标注在方法上,该方法将在类被初始化后调用。在Spring Boot应用中,你可以使用这个注解来执行一些初始化的逻辑。
1 2 3 4 5 @PostConstruct public void doSomething () { System.out.println("do something" ); }
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" ); } }
@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" ); } }
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" ); } }
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)
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://gi thub.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.separator
或Paths
类来构建路径。
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.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 发起请求,上传文件:
本机查看指定上传目录,当然上传成功了:
文件下载
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))); BufferedOutputStream outputStream = new BufferedOutputStream (response.getOutputStream()); response.setContentType("image/png" ); response.setHeader("Content-Disposition" , "attachment;filename=" + downloadFileName); response.addHeader("Pargam" ,"no-cache" ); response.addHeader("Cache-Control" ,"no-cache" ); 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 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 ( ) { const requestBody = { licenseId : "dba9097c-b866-430c-8eb8-15a05f37b4da" , }; 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 (); }) .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 ); }); } </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 ( ) { const formData = new FormData (); formData.append ("licenseId" , "dba9097c-b866-430c-8eb8-15a05f37b4da" ); const xhr = new XMLHttpRequest (); xhr.open ( "POST" , "http://localhost:8081/backend-server/license/test" , true ); xhr.responseType = "blob" ; xhr.onload = function ( ) { if (xhr.status === 200 ) { const blob = xhr.response ; const url = URL .createObjectURL (blob); const a = document .createElement ("a" ); a.style .display = "none" ; a.href = url; a.download = "new.png" ; document .body .appendChild (a); a.click (); 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)));BufferedOutputStream outputStream = new BufferedOutputStream (response.getOutputStream());
发送 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' ); const xhr = new XMLHttpRequest (); xhr.open ('POST' , 'http://localhost:8081/backend-server/license/test' , true ); xhr.onload = function ( ) { if (xhr.status === 200 ) { 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 const a = document .createElement ("a" ); a.style .display = "none" ; a.href = url; a.download = formData.licenseId + ".lic" ; document .body .appendChild (a); a.click ();
这么写,设置下载的文件名:
1 2 a.download = formData.get ("licenseId" ) + ".lic" ;
网络文件
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); } }
客户端下载
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 @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))); BufferedOutputStream outputStream = new BufferedOutputStream (response.getOutputStream()) ) { response.setContentType("image/png" ); 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" ))); } }
日期
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) { ZonedDateTime zonedDateTime = localDateTime.atZone(ZoneId.systemDefault()); return Date.from(zonedDateTime.toInstant()); }
1 2 3 4 5 6 7 8 9 10 11 private LocalDateTime convertDateToLocalDateTime (Date date) { Instant instant = date.toInstant(); ZonedDateTime zonedDateTime = instant.atZone(ZoneId.systemDefault()); 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)
一点多开始看这官网学习了一个多小时,了解到常见的智能巡检设备,智能巡检机器人,智能红外等。
专业的电网数智化转型服务提供商 - 格蒂电力 (grid-elec.com)
变电站远程智能巡视解决方案-华雁智能科技(集团)股份有限公司 (whayer.cn)
大立科技 - 专注红外三十年 - 专业红外热像仪,红外热成像,红外测温仪,人体测温仪厂家 (dali-tech.com)
本机硬件信息
2024 年 12 月 24 日
Windows10使用批处理获取电脑的详细信息并在指定路径保存该信息_批处理获取电脑硬件信息-CSDN博客
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 offecho 开始获取本机信息,请稍等...... 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 offset OUTPUT_FILE=D:\OutputFile.txtecho 开始获取本机信息... 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-1 C 物理地址. . . . . . . . . . . . . : 80-45 -DD-E3-6 E-E0 物理地址. . . . . . . . . . . . . : 82-45 -DD-E3-6 E-DF 物理地址. . . . . . . . . . . . . : 00-50 -56 -C0-00 -01 物理地址. . . . . . . . . . . . . : 00-50 -56 -C0-00 -08 物理地址. . . . . . . . . . . . . : 80-45 -DD-E3-6 E-DF 物理地址. . . . . . . . . . . . . : 80-45 -DD-E3-6 E-E3 CPU序列号: BFEBFBFF000806C1 主板序列号: YX02JN9N
仅获取 MAC 地址,批处理:
1 2 3 4 5 6 7 8 echo 开始获取本机信息... @echo offfor /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 offset OUTPUT_FILE=D:\OutputFile.txtecho 开始获取本机信息... @echo offecho 本机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-1 C 80-45 -DD-E3-6 E-E0 82-45 -DD-E3-6 E-DF 00-50 -56 -C0-00 -01 00-50 -56 -C0-00 -08 80-45 -DD-E3-6 E-DF 80-45 -DD-E3-6 E-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 offset OUTPUT_FILE=D:\OutputFile.txtecho 开始获取本机信息... 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-1 C 80-45 -DD-E3-6 E-E0 82-45 -DD-E3-6 E-DF 00-50 -56 -C0-00 -01 00-50 -56 -C0-00 -08 80-45 -DD-E3-6 E-DF 80-45 -DD-E3-6 E-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 模块即可,这个项目会越发庞大的,也许吧。
那时候再考虑重构,早着呢。
两点半了,第一阶段项目重构完毕。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @SpringBootApplication public class CommonServerApplication { public static void main (String[] args) { SpringApplication.run(CommonServerApplication.class, args); } }
三点整,重构完毕,开始写增删改查。
配置驼峰下划线自动转换(数据库字段是下划线,类属性为驼峰):
1 2 3 4 5 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]
关于打包时出现程序包不存在的情况_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 >
启动失败:
在Linux中杀死占用某个端口的进程_linux 杀死端口号进程-CSDN博客
1 netstat -tunlp | grep 8081
起来了,起来了。
成功访问后台管理接口文档:后台管理接口文档
2024 年 12 月 30 日
搞懂单点登录SSO,基于SpringBoot+JWT实现单点登录解决方案-腾讯云开发者社区-腾讯云 (tencent.com)
改造用户授权验证为 Token。
1 2 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 <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 public void setExpire (String key, long time) { redisTemplate.expire(key, time, TimeUnit.SECONDS); }
写个测试类吧,连接下服务器 Redis,简单写个增删改查试试看。
1 2 3 spring.redis.host =redis.tellhow.com spring.redis.port =6379
测试代码,模拟用户登录。
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" )); }
成功。
用户登录,生成 Token,保存并返回。
1 2 3 4 5 6 7 8 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;
相当成功。
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 @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 " )) { filterChain.doFilter(request, response); return ; } token = token.substring(7 ); boolean verifyToken = jwtUtil.verifyToken(token); if (!verifyToken) { log.error("当前token已过期" ); response.addHeader("message" , "not login" ); response.setStatus(403 ); return ; } 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 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 ; }
我想明白了。
是的,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 ; } 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 ; } 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 public LicenseContentVO generateLicense (LicenseCreatorParamRequest param, HttpServletRequest request) { Boolean validated = this .validateAndSetParam(param); ThrowUtils.throwIf(!validated, ErrorCode.PARAMS_ERROR, "证书生成参数校验失败" ); 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 @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 ; } 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 @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 ; } 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。
不止一次。
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); whiteListPaths.add(UserConstant.SWAGGER_RESOURCES_WHITE_PATH); whiteListPaths.add(UserConstant.SWAGGER_RESOURCES_CONFIGURATION_UI_WHITE_PATH); whiteListPaths.add(UserConstant.SWAGGER_API_DOCS_WHITE_PATH); }
特么服务端怎么出这么大幺蛾子,不知道又在加载什么路径,总之过滤器代码没有限制住请求,该放行的没有放行。
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 ; }
1 2 3 4 5 6 7 log.error("token为空" );try (PrintWriter out = httpResponse.getWriter()) { 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" ); 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 ; }
这样就好多了。
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 ); 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 ); 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 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 request.getSession().setAttribute(UserConstant.USER_LOGIN_STATE, userInfo);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 @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 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(); 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 ; Integer LICENSE_NAME = 30 ; Integer APPLICANT_NAME = 10 ; Integer VALIDITY_DAYS_MAX = 365 ; Integer VALIDITY_DAYS_MIN = 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 @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 { @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);
默认设置 create_time 字段不随修改而更新,这是个问题:
新增默认排序字段:
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;@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);
特么服气了,下载证书,部署到服务器上,连项目根目录路径都变了。。
真不信了。
原来一直以来部署上去的代码证书都生成失败了,下载就更不用说,因为路径的问题。
这里应该要完善成新增目录,没有目录就递归新建目录,这样不论是服务端还是本地都不会有问题。
还是细节不到位。
艹。
保存到数据库的证书序号 UUID 跟文件目录下的证书序号特么不一致?
线上操作的所有证书生成都是失败的,数据库保存信息倒没出错,所以序号是一致的,但数据库有的,文件目录下边不一定有。
两端都统一出这个错,下载证书:
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)
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 .
搞了半天跨域,还是失败了,但为什么证书下载是这个样子。
1 2 3 4 首先,确保服务器端在响应中正确设置了以下头部信息: Content-Disposition: 这个头部应该设置为 attachment; filename="filename.ext" ,其中 "filename.ext" 是你想要下载的文件名。这个头部告诉浏览器这是一个附件,应该触发下载。 Content-Type: 这个头部应该设置为文件的 MIME 类型,例如 application /pdf 对于 PDF 文件,或者 application /octet-stream 对于未知类型的二进制文件。
Java使用流实现浏览器自动下载文件(附前端请求代码)_java图片下载代码实现-CSDN博客
指定下载个图片可以,但证书下载其实也没问题,问题在于如何触发浏览器的下载功能。
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))); BufferedOutputStream outputStream = new BufferedOutputStream (response.getOutputStream()); response.setContentType("image/png" ); response.setHeader("Content-Disposition" , "attachment;filename=" + downloadFileName); response.addHeader("Pargam" ,"no-cache" ); response.addHeader("Cache-Control" ,"no-cache" ); 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 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 ( ) { const requestBody = { licenseId : "dba9097c-b866-430c-8eb8-15a05f37b4da" , }; 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 (); }) .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 ); }); } </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 ( ) { const formData = new FormData (); formData.append ("licenseId" , "dba9097c-b866-430c-8eb8-15a05f37b4da" ); const xhr = new XMLHttpRequest (); xhr.open ( "POST" , "http://localhost:8081/backend-server/license/test" , true ); xhr.responseType = "blob" ; xhr.onload = function ( ) { if (xhr.status === 200 ) { const blob = xhr.response ; const url = URL .createObjectURL (blob); const a = document .createElement ("a" ); a.style .display = "none" ; a.href = url; a.download = "new.png" ; document .body .appendChild (a); a.click (); 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)));BufferedOutputStream outputStream = new BufferedOutputStream (response.getOutputStream());
发送 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' ); const xhr = new XMLHttpRequest (); xhr.open ('POST' , 'http://localhost:8081/backend-server/license/test' , true ); xhr.onload = function ( ) { if (xhr.status === 200 ) { 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 const a = document .createElement ("a" ); a.style .display = "none" ; a.href = url; a.download = formData.licenseId + ".lic" ; document .body .appendChild (a); a.click ();
这么写,设置下载的文件名:
1 2 a.download = formData.get ("licenseId" ) + ".lic" ;
证书撤销,很简单的实现:
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 @ApiOperation(value = "License 证书撤销", notes = "License 证书撤销") @PostMapping("/revoke") public BaseResponse<Boolean> downloadLicense (@RequestBody LicenseRevokeRequest licenseRevokeRequest, HttpServletRequest request) { 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, "非管理员, 无权限下载证书" ); 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, "撤销证书失败, 请稍后重试" ); 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 public static <T> BaseResponse<T> success (T data) { return new BaseResponse <>(0 , data, "ok" ); }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 @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, "创建项目成功" ); }
记得改这个:
指定证书不存在:
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); ThrowUtils.throwIf(StringUtils.isBlank(filePath) || !Files.exists(path), ErrorCode.PARAMS_ERROR, "指定证书不存在, 证书下载失败" );String downloadFileName = licenseId + ".lic" ;
公共属性字段填充。
项目状态枚举:
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/'
死活推不上去 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
iois-backend-server: 项目管理,许可证管理 (gitee.com)
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 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 @Override public Boolean upload (MultipartFile file) { String originalFilename = file.getOriginalFilename(); assert originalFilename != null ; 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: 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 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(); }
艹。看见了:
1 2 3 4 5 6 7 8 9 10 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 @ApiOperation(value = "License 证书安装", notes = "License 证书安装") @PostMapping("/install") public CommonResult<LicenseContent> install (@RequestBody LicenseInstallRequest licenseInstallRequest) { LicenseContent result = licenseVerifyService.install(licenseInstallRequest); 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 @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 @ApiOperation(value = "License 证书安装", notes = "License 证书安装") @PostMapping("/install") public CommonResult<LicenseContent> install (@RequestBody LicenseInstallRequest licenseInstallRequest) { 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 , "证书序号不符合规范" ); } LicenseContent result = licenseVerifyService.install(licenseInstallRequest); 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 @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 public synchronized LicenseContent install (LicenseVerifyParam param) { log.info("++++++++ 开始安装证书 ++++++++" ); LicenseContent result = null ; DateFormat format = new SimpleDateFormat ("yyyy-MM-dd HH:mm:ss" ); 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 , "证书安装失败" ); } log.info("++++++++ 证书安装结束 ++++++++" ); return result; }
怪了,这怎么拼接成这个样子。
1 2 3 String licenseId = request.getLicenseId();String originalFilename = Paths.get(licenseId, LicenseConstant.LICENSE_FILE_EXTENSION).toString();
艹,这是个专门拼接文件目录的方法。
简单的拼接字符串还是用简单的 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);
证书安装前校验证书和公钥是否存在,同样的证书生成前也要校验私钥是否存在,证书和公钥下载前也需要校验是否存在。
公钥下载,证书安装,证书校验。
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); ThrowUtils.throwIf(StringUtils.isBlank(filePath) || !Files.exists(path), ErrorCode.PARAMS_ERROR, "指定证书不存在, 证书下载失败" ); String downloadFileName = "publicCert.keystore" ; try { BufferedInputStream inputStream = new BufferedInputStream (Files.newInputStream(path)); BufferedOutputStream outputStream = new BufferedOutputStream (response.getOutputStream()); response.setContentType("application/octet-stream" ); 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); }
证书生成时的校验,指定生成证书后存放目录,检查私钥是否存在,这一步就完善了生成证书时私钥不存在的提示信息。
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 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 LicenseInfo licenseInfo = new LicenseInfo (); licenseInfo.setId(licenseId); licenseInfo.setStatus(LicenseStatusEnum.REVOKED.getValue());boolean updateById = licenseInfoService.updateById(licenseInfo); ThrowUtils.throwIf(!updateById, ErrorCode.OPERATION_ERROR, "撤销证书失败, 请稍后重试" );return ResultUtils.success(true , "License 证书撤销成功" );
1 2 3 4 5 6 7 8 9 10 11 12 13 LicenseInfo licenseInfo = new LicenseInfo (); licenseInfo.setId(licenseId); licenseInfo.setStatus(LicenseStatusEnum.VALID.getValue());boolean updateById = licenseInfoService.updateById(licenseInfo); ThrowUtils.throwIf(!updateById, ErrorCode.OPERATION_ERROR, "恢复证书失败, 请稍后重试" );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 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 @ApiOperation(value = "公钥上传", notes = "公钥上传") @PostMapping("/public/key/upload") public CommonResult<Boolean> uploadPublicKey (MultipartFile file) { if (ObjectUtils.isEmpty(file)) { return CommonResult.failed("公钥上传参数不能为空" ); } Boolean result = licenseVerifyService.uploadLicense(file); 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 @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 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<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; } }
有好多抛出异常的工具类没写好,适当的业务逻辑处也要记录日志,这以后就是编码习惯了。
原来分页查询后,封装 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 @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 @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 @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 <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 { 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 @Aspect @Component public class AuthInterceptor { @Resource private UserInfoService userInfoService; @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 @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 @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 证书生成成功" ); }
成功了。
测试了一遍后台管理系统,服务端基本没有问题了,推一下代码,重新部署一下。
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 @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 @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 , "指定的目录不存在或不是一个有效的目录" ); } try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir.toPath(), "*.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 证书,就是有些许简陋,不过仅仅返回证书序号即可。
License 证书客户端下各个模块的配置文件,可不要写串了:
1 2 3 4 5 6 7 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: 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
可以考虑重新换证书密钥对了,不过对于开发环境下还为时过早,更换密钥对意味着先前生成的所有证书都将即刻作废。
为了近期证书的测试方便,暂且不需要重新生成密钥对。
下午又发现个问题,客户端验证证书竟然没有填充额外信息么,额外信息应该包括本机硬件信息的。
怪了,生成证书这里没有问题,信息确实封装到证书中去了。
尝试安装这封新的证书。
罢了,安装新证书也是同样的效果,证书生成后包含了额外信息,但安装完成以后竟然没有额外信息了,其中一定有环节出问题了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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 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 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 , "证书安装失败" ); } log.info("++++++++ 证书安装结束 ++++++++" ); return result; }
手动拖动上传也没有解决问题,要么生成时没有携带硬件信息,要么就是安装时没有绑定到相关硬件信息。
问题出在这里吗:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @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 @Data public class LicenseCheckModel { private List<String> ipAddress; private List<String> macAddress; private String cpuSerial; 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 ...
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 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 @Override protected synchronized void validate (final LicenseContent content) throws LicenseContentException { super .validate(content); LicenseCheckModel expectedCheckModel = (LicenseCheckModel) content.getExtra(); if (expectedCheckModel != null ) { LicenseCheckModel serverCheckModel = getServerInfos(); if (serverCheckModel != null ) { if (expectedCheckModel.getCheckIp() && !checkIpAddress(expectedCheckModel.getIpAddress(), serverCheckModel.getIpAddress())) { throw new LicenseContentException ("当前服务器的IP没在授权范围内" ); } if (expectedCheckModel.getCheckMac() && !checkIpAddress(expectedCheckModel.getMacAddress(), serverCheckModel.getMacAddress())) { throw new LicenseContentException ("当前服务器的Mac地址没在授权范围内" ); } if (expectedCheckModel.getCheckMainBoard() && !checkSerial(expectedCheckModel.getMainBoardSerial(), serverCheckModel.getMainBoardSerial())) { throw new LicenseContentException ("当前服务器的主板序列号没在授权范围内" ); } if (expectedCheckModel.getCheckCpu() && !checkSerial(expectedCheckModel.getCpuSerial(), serverCheckModel.getCpuSerial())) { throw new LicenseContentException ("当前服务器的CPU序列号没在授权范围内" ); } } else { throw new LicenseContentException ("不能获取服务器硬件信息" ); } } }
我决定不使用该校验逻辑,CustomLicenseManager
这个自定义类复写了很多内置方法,包括证书生成,安装和校验逻辑。
目前不需要。
现在的证书安装和校验逻辑不完善。
安装和校验成功与否,核心在于证书主题和密钥对校验,这部分信息校验已经在内置 API 中完成,不然安装一定会失败的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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 @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 @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 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 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 @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.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 { @ApiModelProperty(value = "可被允许的IP地址", required = true) private List<String> ipAddress; @ApiModelProperty(value = "可被允许的 MAC 地址", required = true) private List<String> macAddress; @ApiModelProperty(value = "可被允许的 CPU 序列号", required = true) private String cpuSerial; @ApiModelProperty(value = "可被允许的主板序列号", required = 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 @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 @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 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 @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 @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" ; }@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
果然是这样,成功了。
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] . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | ' _ | '_| | ' _ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: 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 String IP_ADDRESS_ENV = "IP_ADDRESS" ;String MAC_ADDRESS_ENV = "MAC_ADDRESS" ;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())); 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 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(); 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(); 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 public LicenseCheckModel getHardwareInfoFromEnv () { 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) || 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 = 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 , "证书安装失败" ); }
好像成功了,稀里糊涂的,再测试一下。
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 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.
远程部署成功!
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 , "证书安装失败" ); }
去掉 try-catch
块返回的提示信息可能会更加直观一些,不然可能会吞掉异常。
傻了,用不着那么干,这样就行:
1 2 3 4 5 } catch (Exception e) { log.error("证书安装失败" , e); throw new CommonException (401 , e.getMessage()); }
现在的问题是,为什么安装新证书失败后,项目直接死掉了。
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 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 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 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 <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > <scope > runtime</scope > </dependency >
1 2 3 4 5 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
借助 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 '项目信息' ;
建库建表完成,导入 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 >
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 注解:
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 中非常重要,尤其是在将对象用作哈希表(如 HashMap
、HashSet
等)的键时,或者当你需要比较对象是否相等时。
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博客
部署 我特么竟然一步到位部署成功了,真就当我什么都会是吧,还好我什么都会一点。
打 jar 包:
找到 jar 包,本地 Linux 虚拟机远程连接服务器:
1 2 3 192.168.118.16 root rootmax
在 /home 目录下创建自己的目录:
1 /home/m emory/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 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 pid=`ps -ef|grep $APP_NAME | grep -v grep | awk '{print $2}' `kill -9 $pid echo "$pid 进程终止成功" sleep 2if test -e $APP_NAME then echo '文件存在,开始启动此程序...' nohup java -jar $APP_NAME --spring.config.location=$CONFIG_FILE_PATH > $LOG_FILE 2>&1 &tail -f $LOG_FILE else echo '$APP_NAME 文件不存在,请检查。' fi
分别把 jar 包,日志文件,配置文件放在指定目录下,这个脚本中的配置相应也要修改成指定目录。
打开页面拖进来就行了,拖到指定目录下。
执行脚本之前,先赋予可执行权限,要不然会报错:
1 chmod +x ./restart_license_server.sh
执行脚本:
1 ./restart_license_server.sh
如你所见,启动成功:
访问下 Swagger 接口文档地址:http://192.168.118.16:8081/license-server/doc.html
1 http:// 192.168 .118.16 :8081 /license-server/ doc.html
用户管理模块开发完毕后就可以着手部署启动项目了。
启动失败:
在Linux中杀死占用某个端口的进程_linux 杀死端口号进程-CSDN博客
1 netstat -tunlp | grep 8081
起来了,起来了。
成功访问后台管理接口文档:后台管理接口文档
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: The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server. 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: The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server. 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
明天再看吧,现在头脑有点迷糊。
2024 年 12 月 27 日
果然还是域名解析的问题,在我本机和虚拟机远程连接的 Linux 服务器上启动项目时都出现了这样的问题。
1 2 3 4 5 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
这下接口文档正常运行了。
不过这毕竟不是长久之计,还是得研究下怎么解决配置 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 mybatis-plus: configuration: map-underscore-to-camel-case: false 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.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 找到: 没有参数 原因: 实际参数列表和形式参数列表长度不同
加个无参构造函数即可。
艹,还是不管用,这两种构造方法只能用其中一个。
特么出现这个报错:
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
分析:
ExceptionInInitializerError
:
这个错误通常表明在静态初始化块或静态变量初始化时发生了异常。在您的 DefaultDBFieldHandler
类(假设这是您自定义的 MetaObjectHandler
实现)中,可能有一个静态初始化块或静态变量初始化时抛出了异常。
检查 DefaultDBFieldHandler
类中的静态代码块和静态变量初始化代码,确保没有抛出任何未捕获的异常。
直接写这里边吧。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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'
排查出来了,上面是数据库的问题。
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()); } 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 @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 { @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)
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 > <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 的开源项目代码,可以拿取到。
代码统计
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
新项目,代码量不少。
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 $ find . "(" -name "*.cs" -or -name "*.resx" ")" -print | xargs grep -v "^$" | wc -l
周例会
2025 年 1 月 9 日
1 @所有人 近期有发现tfs上生产分支代码和生产环境实际部署的应用不一致的问题。 现在本周开始代码自查,没有提交tfs的抓紧提交。 下周开始互查和随机抽查, 一经发现代码不一致,发现一处扣绩效1000元。发现多处的,记录下来,后面分多个月扣除,每个月扣一次。(参与检查的项目范围为2024年研发参与的所有项目,包括上半年已经交付的项目)
1 @所有人 tfs任务记得及时关闭,上周少关的任务及时补上。明天例会就tfs上生产分支代码和生产环境实际部署的应用不一致的问题请每个人都准备好发言意见。
配置文件
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博客
艹。
配置静态 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 地址值。
保存并退出编辑器 : 在 vim
中,您可以按 Esc
键,然后输入 :wq
并按回车来保存更改并退出编辑器。
重启网络服务 : 使用以下命令重启网络服务以应用更改:
1 2 3 bash复制代码 sudo systemctl restart network
ping不通Linux服务器的原因?_windows ping不通linux-CSDN博客
如何排查和解决Linux服务器配置IP后Ping不通的问题?-桔子数据 (95vps.com)
网络服务没在正常运行?
艹。
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);
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 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 , "证书安装失败" ); } 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 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 = 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 , "证书安装失败" ); } 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= 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, "证书安装失败" ); }
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博客
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(1 , 1 ) A(: , 2 ) A(2 , : ) A( : , : ) A( : ) A(1 ) A(2 :4 ) A(1 ,1 ) = 100 ; A(3 , 3 ) = 100 ; A A(1 , : ) = [ ] A A(2 : 2 ) = 666 ; A
编写 Java 代码调用 matlab 接口实现数据分析处理,首先得搞明白 matlab 文件运行情况。
很简单的操作。
Matlab 安装
昨天已经下载解压安装完毕。
百度网盘 - 下载链接
各版本安装教程
解压密码:yiliu。
MCR 安装 matlab mcr安装图标,Matlab运行环境MCR安装-CSDN博客
在安装路径中获取MCR。
进入 matlab 输入 mcrinstaller
,会弹出 mcr 的路径。
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\t ellhow\广西项目\t est\doubleInput2\f or_redistribution\MyAppInstaller_web.exe"
这是打包好 doubleInput 项目以后,在这个目录下找到了运行环境安装可执行文件,同我上面解压出来的运行环境安装包是一样的。
最终都在本机配置了相应的环境变量。
不过很明显,后者要占用磁盘空间更大一些,估计这是未压缩版。
那么如果是在 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 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] -bash: unzip: 未找到命令 [root@iZ2ze4hnl6pls28qt4w1ttZ matlab] 已加载插件: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] 总用量 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 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 2 (base) [root@thmn analysis] /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] 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 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 2 export MCR_HOME=/usr/local/MATLAB/MATLAB_Runtime/R2024aexport LD_LIBRARY_PATH=$LD_LIBRARY_PATH :$MCR_HOME /runtime/glnxa64:$MCR_HOME /bin/glnxa64:$MCR_HOME /sys/os/glnxa64
成功了,终于成功了。
1 2 [root@iZ2ze4hnl6pls28qt4w1ttZ R2024a] /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] 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] 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] 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 ~] 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 ~] 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 (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 ~] /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/g lnxa64:$LD_LIBRARY_PATH
linux中环境变量配置文件 - 腾讯云开发者社区 - 腾讯云 (tencent.com)
2025 年 2 月 14 日
当然还会报错了:
1 2 export MCR_HOME=/usr/local/MATLAB/MATLAB_Runtime/R2024aexport 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] 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/R2024aexport 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] 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] 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/R2024aexport 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] 3.10.0-1160.119.1.el7.x86_64 [root@iZ2ze4hnl6pls28qt4w1ttZ matlab] CentOS Linux release 7.9.2009 (Core) [root@iZ2ze4hnl6pls28qt4w1ttZ matlab]
太特么恶心了吧。
官网上为什么没有版本适配信息。
我把配置文件又改回来了。
1 2 export MCR_HOME=/usr/local/MATLAB/MATLAB_Runtime/R2024aexport 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] 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 find / -name libstdc++.so.6
1 2 3 4 5 6 7 8 9 10 11 12 13 14 [root@iZ2ze4hnl6pls28qt4w1ttZ matlab] /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/R2024aexport 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] 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/R2024aexport 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/R2024aexport 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' ; url = 'jdbc:mysql://localhost:3306/your_database_name' ; javaaddpath('path_to_your_mysql_connector.jar' ); conn = database(dbname, username, password, jdbc_driver, url);
1 2 3 4 5 6 7 8 query = 'SELECT * FROM your_table_name' ; 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('数据分布饼状图' );
如何下载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/my sql-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' ; url = 'jdbc:mysql://localhost:3306/iois_backend' ; javaaddpath('E:\Matlab\java\jar\toolbox\mysql\mysql-connector-java-8.0.21.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 query = 'SELECT * FROM license_info' ; 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 数据库连接失败: 输入参数太多。
妈的这样就输入参数太多,那这样呢。
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" ; 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. ...'
这是测试查询的数据:
那其实就像上午讲的那样,不论是使用 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 function F = A F.add = @add; F.multiply = @multiply; F.mis = @mis;end 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 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 -javaans = '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 -version
和 javac -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
窗口如下:
选择其中的 Library Compiler
点击,就进入了代码打包窗口:
打包成功,同时打包好的.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 (); Object[] result = class1.doubleInput(1 , 6 ); if (result != null && result.length > 0 ) { System.out.println(result[0 ]); } else { System.out.println("没有返回结果" ); } } catch (MWException e) { e.printStackTrace(); throw new RuntimeException ("MATLAB 函数调用失败" , e); } }
接下来考虑下,这个过程中有安装 MCR 么,没有 Matlab 编译运行时环境还能不能跑这段代码?
特么当然用到了。
1 "D:\Project\t ellhow\广西项目\t est\doubleInput2\f or_redistribution\MyAppInstaller_web.exe"
这是打包好 doubleInput 项目以后,在这个目录下找到了运行环境安装可执行文件,同我上面解压出来的运行环境安装包是一样的。
最终都在本机配置了相应的环境变量。
不过很明显,后者要占用磁盘空间更大一些,估计这是未压缩版。
那么如果是在 Linux 系统下,前者 .exe 可执行文件也许就不适用了,只能采用解压运行环境压缩包来配置环境,还需要手动配置环境变量。
不好意思看走眼了,两个都是 .exe 文件格式。
那么现在本机已经成功实现使用 Java 调用 matlab 打包的 jar 包程序,调用数据分析接口。
我可以试一试,先测试运行环境变量安装,成功后再研究调用本身。
手动构建工程打成 jar 包,运行成功。
这就是 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) 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' ; 2 , 150 , 'B' ; 1 , 200 , 'C' ; 3 , 120 , 'D' ]; bus = [ 1 ; 2 ; 3 ]; gen_bus_numbers = gen(:, 1 ); Mbg = getMbgMatrix(gen_bus_numbers, bus); disp ('Mbg Matrix:' ); disp (Mbg); expected_Mbg = [ 1 , 0 , 1 , 0 ; 0 , 1 , 0 , 0 ; 0 , 0 , 0 , 1 ]; 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" ; 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' ; 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)
原来自定义分区是这么操作的。
1 memory memory -pc Dw990831@
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) (二月 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 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 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 (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
试下这条命令。
银河麒麟V10系统默认不能ping通的解决方案-加固计算机,加固笔记本,军用计算机,加固平板电脑,三防电脑,加固笔记本,加固服务器,三防笔记本-四川长风致远科技有限公司-官方网站 (chinfort.com)
国产操作系统银河麒麟V10,遇到了不能ping通的问题,能上网,也能ping通别的电脑,大概率是防火墙的问题,通过修改防火墙设置可以正常使用ssh。
右键桌面空白处打开终端,输入 sudo iptables-F
后回车,此命令用于清空关闭防火墙。提示输入系统密码,完成后再次回车,此时已经可以通过局域网内的其他电脑ping通麒麟主机,且XSHELL等也可以正常使用。
为了重启后能够让防火墙状态继续保持关闭,还需做如下修改。继续在终端中输入 sudo pluma /etc/rc.loca
l 后回车,然后在如下界面的第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/m emory/matlab/ Matlab_R2018b/R2018b/ version.txt inflating: /home/m emory/matlab/ Matlab_R2018b/R2018b/ VersionInfo.xml inflating: /home/m emory/matlab/ Matlab_R2018b/公众号免责声明.txt inflating: /home/m emory/matlab/ Matlab_R2018b/公众号网站:羽享平台.url inflating: /home/m emory/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] (base) [root@thmn R2018b] 总用量 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 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] -bash: ./install: 权限不够 (base) [root@thmn R2018b] (base) [root@thmn R2018b] ./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] -bash: cd : /home/memory/matlab/Matlab_R2018b/R2018b/bin/glnxa64/install_unix: 不是目录 (base) [root@thmn R2018b] (base) [root@thmn glnxa64] 总用量 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]
1 2 3 4 (base) [root@thmn R2018b] 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] Preparing installation files ... Installing ... --------------------------------------------------------------------------- 错误: 安装无法继续。您可能需要执行以下任一操作: 1.设置 X11 显示,然后重新启动安装过程 2.通过指定 -mode silent 选项使用静默安装功能 --------------------------------------------------------------------------- Finished
直接执行,静默安装。
安装失败。
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 ./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]cp : 略过目录"/home/memory/matlab/Matlab_R2018b/Crack"
复制目录,搞错了。
1 (base) [root@thmn Crack]
1 (base) [root@thmn Crack]
vmware虚拟机连接服务器超时,vmware连接远程服务器超时-CSDN博客
1 2 3 4 5 6 7 使用FinalShell连接远程服务器后安装Matlab软件没有窗口弹出、没有安装页面引导,只有命令行的情况,通常是因为在远程服务器上运行图形界面软件时,受限于远程会话的环境,图形界面无法直接显示在本地计算机上。 针对这种问题,有几种可能的解决方案: 使用X 11 转发: 如果你的本地计算机和远程服务器都支持X 11 协议,你可以在SSH连接时启用X 11 转发。这样,远程服务器上运行的图形界面程序可以将图形输出发送到本地的X 服务器进行显示。 在FinalShell中,你可以在SSH连接设置中找到并启用“X 11 转发”选项。然后,在远程服务器上运行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 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 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 ./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
命令的参数说明如下:
Enforcing
或 1
:这是 SELinux 的默认模式,也称为强制模式。在这个模式下,SELinux 策略被严格执行。如果某个操作违反了策略规则,那么该操作将被阻止,并且通常会记录一条拒绝消息。
Permissive
或 0
:在这个模式下,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 /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 (base) [root@thmn analysis]
麒麟这边又出什么问题。
1 2 3 4 5 6 7 8 9 root@memory-pc:/home/memory/mcr/MCR/v95 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
好像又没啥问题了。
16 这边执行。
1 2 3 4 5 6 (base) [root@thmn helloworld] ------------------------------------------ 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 2 3 4 5 6 7 8 9 10 11 12 13 14 root@memory-pc:/home/memory/matlab/projects/helloworld KySec status: enabledexec 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
呵呵呵,显然麒麟V10也运行成功了。
1 2 3 4 5 6 root@memory-pc:/home/memory/matlab/projects/helloworld ------------------------------------------ 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 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 总用量 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 root@memory-pc:/home/memory/mcr root@memory-pc:/home/memory/mcr root@memory-pc:/home/memory/mcr root@memory-pc:/home/memory/mcr root@memory-pc:/home/memory/mcr
1 2 3 4 5 6 root@memory-pc:/home/memory/mcr 总用量 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.
安装 Excel 或 WPS :
如果你需要将数据写入 Excel 文件,最简单的解决方案是安装 Microsoft Excel 或 WPS。安装后,MATLAB 将能够正常调用 writematrix
函数。
使用其他文件格式 :
使用 xlswrite
函数 :
**使用 writecell
或 writetable
**:
如果你需要将数据写入 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 正在选中未选择的软件包 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) 的触发器 ...
如果提示缺少依赖,继续下一步。
解决依赖问题(如果需要),这个命令会自动修复安装过程中可能遇到的依赖问题。
虽然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 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 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 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 ------------------------------------------ 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 ------------------------------------------ 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 ------------------------------------------ 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/m emory/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 ------------------------------------------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 ./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/matlabThe 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 deniedscp : 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/matlabth @192.168.118.120 's password:scp : dest open "/home/memory/matlab/OutputFile.txt" : Permission deniedscp : failed to upload file D:/\346 \241 \214 \351 \235 \242 /OutputFile.txt to /home/memory/matlab
原来是非管理员,没有权限。
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/ 1 ka6MauAPAaxcfs3L7XUMOQ 提取码: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 /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。
直接执行安装。
选择使用文件安装密钥安装;输入文件秘钥;选择安装位置;等待安装完成。
安装成功后,复制缺失文件至安装根目录下。
打开 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
MCR 配置 安装兼容该 Matlab 编译器版本的 MCR ,需在 Matlab 命令行窗口下输入以下命令获取可用的 MCR 压缩包路径。
若 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 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
立即生效更新内容,检验环境变量是否修改成功。
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_timingchaincloud
,case_DG_timingchaincloud
,DG_potential1
、DG_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 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/v95echo "/run_data_DG_timingchaincloud.sh 执行完成" echo "开始执行 ./run_case_DG_timingchaincloud.sh" /case_DG_timingchaincloud/run_case_DG_timingchaincloud.sh /home/memory/mcr/MCR/v95echo "./run_case_DG_timingchaincloud.sh 执行完成" echo "开始执行 ./run_DG_potential1.sh" ./DG_potential1/run_DG_potential1.sh /home/memory/mcr/MCR/v95echo "./run_DG_potential1.sh 执行完成" echo "开始执行 ./run_DG_potential2.sh" /DG_potential2/run_DG_potential2.sh /home/memory/mcr/MCR/v95echo "./run_DG_potential2.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 ~] 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 ~] 总用量 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 ~] 登出 Connection to 172.16.6.33 closed. (base) [root@thmn ~]
现在链接最有趣的是,用 FinalShell 连接 33 服务器 22330 没有问题,但上传下载文件失败,尝试本机、Xshell、Xftp 连接都特么失败了。
【2024最新版可用,可用到2099】详解可成功的idea一键激活,(附安装包&激活文件) - 哔哩哔哩 (bilibili.com)
有趣,IDEA 又快特么过期了。
明天再看这个。
探讨几种在CentOS 7上实现文件上传的方法_centos7上传本地文件-CSDN博客
艹。没法下手。
研究会儿论文。
换个目录竟真的能成功开始上传运行环境,接下来就准备安装了。
等待上传中。
解压。
1 unzip MCR_R2018b_glnxa64_installer -d /home/memory/mcr/MCR_R2018b
期待解压成功。
1 sudo ./install -mode silent -agreeToLicense yes
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
一条龙 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 /u sr/local/ MATLAB/MATLAB_Runtime/ v95
1 2 3 4 5 6 7 8 [root@sesp -test-1 ~] ------------------------------------------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
为什么。
这台服务器真的别扭,磁盘空间不大不说,上传文件,新建文件夹都磨磨蹭蹭的,实在不想再远程连接这家伙了。
在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] ------------------------------------------ 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
出什么问题了这。
未安装 X11 工具包 :系统未安装 libXt
库。
MCR 依赖未满足 :MATLAB Compiler Runtime (MCR) 需要 X11 库支持图形界面。
路径配置问题 :LD_LIBRARY_PATH
未包含 X11 库路径。
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] -bash: ./run_all.sh: /bin/bash^M: 坏的解释器: 没有那个文件或目录
这是换行符问题。
安装 dos2unix
工具,该工具用于将 Windows 格式的文本文件转换为 Unix/Linux 格式。
1 2 sudo yum install dos2unix -y
1 2 3 4 [root@sesp-test-1 projects] [root@sesp-test-1 projects] [root@sesp-test-1 projects] [root@sesp-test-1 projects]
1 2 3 4 [root@sesp-test-1 projects] [root@sesp-test-1 projects] [root@sesp-test-1 projects] [root@sesp-test-1 projects]
特么的,靠转换还是不行,有的命令都乱码了,还是要在 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/v95echo "/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/v95echo "./run_case_DG_timingchaincloud.sh 执行完成" echo "开始执行 ./run_DG_potential1.sh" ./DG_potential1/run_DG_potential1.sh /home/memory/mcr/MCR_R2018b/env/v95echo "./run_DG_potential1.sh 执行完成" echo "开始执行 ./run_DG_potential2.sh" ./DG_potential2/run_DG_potential2.sh /home/memory/mcr/MCR_R2018b/env/v95echo "./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 if isempty (data) disp ('未查询到数据!' );else target_field = 'load_value' ; values = data.(target_field); result_table = table (values, 'VariableNames' , {target_field}); excel_filename = 'output_data.xlsx' ; 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)
Thank You - Free User Registration Success - NetSarang Website (xshell.com)
xshell、xftp、Xmanager绿色破解版下载地址(持续更新) - 翎野君 - 博客园 (cnblogs.com)
Xmanager 6 安装成功了,看看 Xshell 远程连接开启 X11 服务后能不能直接安装 Matlab。
安装正在进行中,目前来看一切顺利。
1 [root@thmn Matlab_R2018b]
1 [root@thmn Matlab_R2018b]
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 2 3 4 (base) [root@thmn test ] ------------------------------------------ Usage: ./run_doubleInput.sh <deployedMCRroot> args
当你看到这样的错误信息时,它表明你尝试运行的 doubleInput
可执行文件(尽管你是通过 run_doubleInput.sh
脚本调用的)实际上是一个依赖于 MATLAB Compiler Runtime (MCR) 的程序。这个错误信息提示你需要指定 MCR 的安装根目录作为程序的第一个参数。
在安装路径中获取MCR。
进入 matlab 输入 mcrinstaller
,会弹出 mcr 的路径。
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。
1 2 3 4 5 6 7 税号和社会信用组织代码是一回事吗 税号和社会信用代码确实是一回事。在我国实施三证合一制度后,税号与统一社会信用代码已经实现了统一。 一、税号与统一社会信用代码的统一 三证合一制度的实施:我国实施的三证合一制度,即将企业依次申请的工商营业执照、组织机构代码证和税务登记证三个证件合为一个证件,目的是提高市场准入效率。“一照一码”则是在此基础上的进一步改革,通过将一个统一的社会信用代码作为企业唯一的识别码,实现了企业信息的整合与共享。 税号与统一社会信用代码的关系:在三证合一制度下,企业的税号实际上就是新的工商营业执照上所记载的统一社会信用代码。这个代码具有唯一性,确保了企业的准确识别。
申请开具发票,登记个人/公司发票信息。
提交完成。
对数视力表和国际标准视力表对照表 - 百度文库 (baidu.com)
发票开具完成了吗,早上快十一点提交的,下午两点半就结束了。
软著申请
2025 年 1 月 6 日
我看看那个第二成绩单系统。
山西大学信息门户网站:http://nehall.sxu.edu.cn/#/newsOld。
软件著作权申请教程(超详细)(2024新版)软著申请-CSDN博客
💻软著申请全攻略 (baidu.com)
保姆级软著申请全过程(计算机软件著作版权申请)-腾讯云开发者社区-腾讯云 (tencent.com)
中国版权保护中心 (ccopyright.com.cn)
艹,实名认证还要手持身份证照片。。
晚上回去再看看。
软件著作权如何申请?个人申请软著需要提供哪些材料? (baidu.com)
要是三两个月前提前想到这一步的话,没准到现在软著就能成功申请下来了,目前从零开始申请流程的话有点赶紧,不太现实。
2025 年 2 月 21 日
如何下载软件著作权电子版 (baidu.com)
(29 封私信 / 31 条消息) 在淘宝上购买软件著作权被学校查到怎么办? - 知乎 (zhihu.com)
社会实践
2025 年 1 月 6 日
三下乡返家乡官网 (youth.cn)
学校方面完全避免问题。
这个大学生三下乡社会实践网站登录不进去,跟学校第二成绩单系统激活账号什么的有关系,总之目前解决不了社会实践凑学分的问题。
证书
2025 年 1 月 7 日
📈三级网络安全管理员的职业前景与优势 (baidu.com)
刚群聊里通知只要在这个网站下查到的证书,都是有效的。
1 关于计算机等级考试的问题,其他学生用的一个网站,zscx.osta .org .cn,这个网站能查询到就算。有同学查询了把结果发上来一下。
技能人才评价证书全国联网查询 (osta.org.cn) `
高级搜索 网络–职业技能鉴定网 (zgks.net) ,这个鸟网站查不到。
中国教育考试网 (neea.edu.cn)
中国计算机技术职业资格网 (ruankao.org.cn)
中国人事考试网 (cpta.com.cn)
《网络与信息安全管理员》2024国家职业技能等级认证(职业方向:网络安全管理员、信息安全管理员、数据安全管理员) (qq.com)
网络与信息安全管理员 (数据安全管理员)职业技能等级证书–JYPC证书网 (xyfsw.net) N
2025 年 1 月 8 日
山西大学大学生第二成绩单管理系统 (sxu.edu.cn)
购票
2025 年 1 月 9 日
这么快一月份就到九号了。
成人票转学生票?误区 (baidu.com)