首页
归档
留言
友链
广告合作
壁纸
更多
美女主播
Search
1
博瑞GE车机升级/降级
5,572 阅读
2
Mac打印机设置黑白打印
4,882 阅读
3
修改elementUI中el-table树形结构图标
4,861 阅读
4
Mac客户端添加腾讯企业邮箱方法
4,641 阅读
5
intelliJ Idea 2022.2.X破解
4,310 阅读
Java
HarmonyOS Next
Web前端
微信开发
开发辅助
App开发
数据库
随笔日记
登录
/
注册
Search
标签搜索
Spring Boot
Java
Spring Cloud
Mac
MyBatis
WordPress
Nacos
Spring Cloud Alibaba
Mybatis-Plus
jQuery
MacOS
Java Script
asp.net
MySQL
IntelliJ IDEA
微信小程序
Typecho
Sentinel
UniApp
asp.net core
Laughing
累计撰写
611
篇文章
累计收到
1,427
条评论
首页
栏目
Java
HarmonyOS Next
Web前端
微信开发
开发辅助
App开发
数据库
随笔日记
页面
归档
留言
友链
广告合作
壁纸
美女主播
搜索到
215
篇与
的结果
2025-03-27
RuoYi AI:一个全栈式 AI 开发平台
壹、简介RuoYi-AI是基于经典开源项目RuoYi深度扩展的AI开发平台,它不仅继承了RuoYi家族的高效开发特性,还支持对接OpenAI、C还GLM、讯飞星火等几十种大语言模型,实现了聊天对话、图像生成、语音克隆等前沿功能,成为开发者构建智能应用的“一站式”解决方案。贰、特色功能全套开源系统:提供完整的前端应用、后台管理以及小程序应用,全部开箱即用。基于MIT开源协议,自由度高,可灵活修改和分发代码。本地RAG方案:集成Milvus/Weaviate向量库、本地向量化模型、Ollama调用本地LLM,实现完全本地化RAG的高效检索与生成,保障数据隐私与性能。丰富插件功能:支持联网、SQL查询插件及Text2API插件,扩展系统能力与应用场景。内置SSE、websocket等网络协议,支持对接多种大语言模型,同时还集成了MidJourney和DALLE AI绘画功能强大的多媒体功能:支持AI翻译、PPT制作、语音克隆和翻唱等扩展功能:支持将大模型接入个人或企业微信支付功能:支持易支付、微信支付等多种支付方式三、开发部署3.1、环境要求JDK17MySQL 5.7或者 MySql 8.0Redis 5.X+Maven 3.8+NodeJS+(含pnpnm)3.2、后端安装3.2.1、Clone代码GiHub地址:https://github.com/ageerle/ruoyi-aiGitee地址:https://gitee.com/ageerle/ruoyi-ai3.2.2、Idea导入项目使用Idea导入项目并正确配置Maven,在application.yaml文件中修改数据库及Redis链接信息3.2.3、初始化数据库数据库初始化脚本位于script/sql/ruoyi-ai.sql。3.2.3、运行项目以上配置完成后,直接运行项目即可。3.3、安装管理端&客户端3.3.1、Clone代码3.3.1.1、管理端代码GiHub地址:https://github.com/ageerle/ruoyi-adminGitee地址:https://gitee.com/ageerle/ruoyi-admin3.3.1.1、客户端代码GiHub地址:https://github.com/ageerle/ruoyi-webGitee地址:https://gitee.com/ageerle/ruoyi-web3.3.2、安装依赖进入ruoyi-admin或者ruoyi-web,打开终端,执行pnpm install3.3.3、运行或打包项目运行项目:pnpm dev打包项目:pnpm build3.4、修改配置3.4.1、申请API KEYhttps://api.pandarobot.chat3.4.2、注册API KEY成功注册账号后点击添加令牌,参数可以全部默认,然后点击复制按钮可以获取API KEY3.4.3、进入后台管理配置默认账号为admin,默认密码为admin123进入运营管理-系统模型-新增模型,在请求密钥处填写上一步申请到的key信息
2025年03月27日
59 阅读
0 评论
0 点赞
2025-03-21
Springboot中使用Undertow替换默认的Tomcat容器
{mtitle title="Undertow介绍"/}Undertow是由JBoss(现为Red Hat)开发的一款轻量级、高性能的Web服务器。它是WildFly应用服务器的默认Web容器,专注于高并发和低延迟的场景。Undertow基于NIO(非阻塞I/O)构建,支持HTTP/1.x、HTTP/2和WebSocket协议。{mtitle title="Undertow优势"/}Tomcat凭借其稳定性、易用性和社区支持成为Springboot默认的容器。虽然Tomcat表现足够优秀,但是Undertow也有其可圈可点的地方。高并发支持:Undertow基于NIO构建,能够高效处理大量并发请求,适合高负载场景。低延迟:由于其非阻塞的设计,Undertow在低延迟场景中表现优异。轻量级:Undertow的核心代码非常精简,启动速度快,资源占用低。在资源有限的情况下,尤其是内存和CPU资源紧张的情况下,Undertow是更好的选择。{mtitle title="Undertow替换Tomcat"/}上面介绍了Undertow的优点,那么我们如何在Springboot项目中使用Undertow作为默认容器呢。先来看一下,默认Tomcat容器,启动成功后控制台输出信息为了使用Undertow容器,我们需要修改项目的pom.xml文件,排除掉Tomcat。 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency>然后添加Undertow的依赖 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-undertow</artifactId> </dependency>然后重启项目,查看控制台输出
2025年03月21日
87 阅读
0 评论
0 点赞
2025-03-18
Java优雅判空技巧:瞧瞧别人家的判空,太香啦!
在 Java 开发中,空指针异常(NullPointerException,简称NPE) 是程序员最常遇到的错误之一。无论是新手还是资深开发者,几乎每个人都曾因为一个突如其来的 NullPointerException 而抓狂过。想象一下这样的场景:项目即将上线,整个团队正忙于最后的部署工作,突然系统抛出了一个堆栈错误。大家花费大量时间排查,最终发现竟然是因为某个变量未初始化。再一看代码,满屏的 if (xxx == null),层层嵌套,复杂到让人头晕目眩。这种低效且低级的判空方式,不仅降低了代码的可读性,还增加了维护成本。然而,判空的方式远不止传统的 if (xxx == null)!今天,我要向大家推荐‘别人家的代码’——那些优雅、高效的判空技巧,简直是代码的艺术品。接下来,我们将一起探讨Java 中如何优雅、高效地进行判空操作*,帮助你彻底告别繁琐的判空代码,提升代码的可读性和可维护性。一、传统判空的血泪史在我们之前的开发过程中,最常见的判空方式,可能就是这样了:if (user != null) { if (user.getAddress() != null) { if (user.getAddress().getStreet() != null) { // 做一些事情 } } }一层又一层的嵌套判空,简直是代码的噩梦!尤其是当这种判空逻辑遍布代码的多个地方时,整个程序的可读性急剧下降,维护成本也随之飙升。更糟糕的是,在调用外部数据接口时,稍有不慎就会引发大量的空指针异常(NPE)。而在高并发场景下,这种问题会被进一步放大,程序崩溃的概率成倍增加,给系统稳定性带来巨大威胁。那么,如何优化这种灾难级别的代码呢?接下来,我们将探讨如何通过现代化的判空技巧,彻底告别多层嵌套判空,提升代码的可读性与健壮性,同时有效避免高并发场景下的空指针异常。二、Java 8+ 时代的判空革命随着Java 8的到来,我们迎来了判空操作的一次革命性升级——Optional!这个神器彻底改变了传统的判空方式,让代码变得更加优雅、简洁。通过链式调用,Optional 不仅避免了繁琐的 if (xxx == null) 判断,还大大提升了代码的可读性和可维护性。无论是新手还是资深开发者,都对它爱不释手。接下来,我们将深入探讨 Optional 的使用技巧,包括基础用法、链式调用以及高级功能,帮助你彻底告别繁琐的判空代码,提升开发效率。1. Optional 黄金三板斧链式调用判空:使用 Optional.ofNullable()Optional 是一个容器对象,它能帮助我们避免空指针异常。通过 Optional.ofNullable(),我们可以优雅地处理可能为 null 的对象,而无需一层层地嵌套判断。让我们看个例子:Optional.ofNullable(user) .map(User::getAddress) .map(Address::getStreet) .ifPresent(street -> { // 在这里使用 street,避免了 NullPointerException });看!这段代码比那段嵌套的 if 优雅多了吧?简洁明了,能够有效避免 NullPointerException,而且还能链式调用,代码的可读性大大提升。高级用法:条件过滤与业务异常抛出Optional 还能用于更复杂的情况,比如在判空时抛出业务异常。比如我们有一个方法,接收一个可能为空的用户对象,并希望如果用户没有地址,就抛出一个自定义的异常:User user = getUser(); String street = Optional.ofNullable(user) .map(User::getAddress) .map(Address::getStreet) .orElseThrow(() -> new IllegalStateException("用户地址不能为空"));当 user 或者 address 为 null 时,代码会自动抛出 IllegalStateException,避免了不必要的 null 检查。2. 封装通用工具类你可能会想,写了这么多判空代码,我能不能封装一个工具类,让其他地方直接调用?当然可以。你可以封装一个 NullSafe 工具类,让判空变得更加简单。比如:public class NullSafe { public static <T> Optional<T> ofNullable(T value) { return Optional.ofNullable(value); } }然后直接调用:String street = NullSafe.ofNullable(user) .map(User::getAddress) .map(Address::getStreet) .orElse("默认街道");三、现代化框架的判空银弹1. Spring 实战技巧如果你在用 Spring 框架,那么 Spring 提供了一些非常好用的工具类来帮助我们判空,比如 CollectionUtils 和 StringUtils。if (CollectionUtils.isEmpty(userList)) { // 处理用户列表为空的情况 } if (StringUtils.isEmpty(user.getName())) { // 处理用户名为空的情况 }这些工具类能大大简化我们的代码,让判空更加直接和简洁。2. Lombok 保驾护航Lombok是我们项目中的老朋友了,@NonNull 注解简直是自动生成判空代码的神奇宝贝。通过它,我们可以在方法参数中标记某个参数不能为 null,如果为 null,Lombok 会自动抛出 NullPointerException,省去了手动检查的麻烦。public void setUserName(@NonNull String name) { this.name = name; }调用这个方法时,如果传入 null,Lombok 会帮我们自动抛出 NullPointerException,这样一来,代码简洁且异常处理得当。四、工程级解决方案在一些大型项目中,空指针检查可能变得尤为复杂,传统的判空方式已经不适用了,这时我们可以考虑采用一些工程级的方案。1. 空对象模式空对象模式是一种很巧妙的解决方案,它通过定义一个空对象来替代 null,避免了频繁的 null 检查。比如,我们可以定义一个 Notification 接口的空对象,实现一个空的 Notification 对象,从而避免频繁的 null 检查。public class EmptyNotification implements Notification { @Override public void notifyUser() { // 什么也不做 } }这样,当没有通知需要发送时,直接使用 EmptyNotification,不需要担心 null 的问题。2. Guava 的 Optional 增强Guava的 Optional 提供了一些更为强大的功能,比如 transform 和 or 方法,能够帮助我们进行更复杂的操作。比如:Optional<String> name = Optional.of("John"); String upperName = name.transform(String::toUpperCase).or("Default Name");这段代码用 Guava 的 Optional 实现了 String 大写转换,并且提供了一个默认值,简洁且优雅。五、防御式编程进阶在一些关键性业务中,我们还可以通过防御式编程来增强系统的健壮性,防止出现 null 值导致的问题。1. Assert 断言式拦截断言(assert)能够帮助我们验证某些关键参数在方法执行之前是有效的。如果某个参数为 null,程序会立即抛出异常。public void processData(@NonNull String data) { assert data != null : "数据不能为空"; // 处理数据 }这样我们就能确保传入的数据不会为 null,提高了代码的健壮性。2. 全局 AOP 拦截AOP 拦截可以帮助我们全局处理参数判空的逻辑,尤其是在接口调用时非常有用。通过自定义注解与 AOP 结合,我们可以在调用接口之前拦截请求,进行判空处理,避免了重复编写判空代码。@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface NotNullCheck { // 自定义判空注解 }然后通过 AOP 拦截:@Aspect @Component public class NullCheckAspect { @Before("@annotation(NotNullCheck)") public void checkParamsNotNull(JoinPoint joinPoint) { for (Object arg : joinPoint.getArgs()) { if (arg == null) { throw new IllegalArgumentException("参数不能为null"); } } } }六、总结在 Java 开发中,判空问题一直是程序员面临的挑战之一。从传统的多层 if (xxx == null) 判空,到 Java 8 引入的革命性工具 Optional,再到现代化框架提供的强大支持,判空操作已经经历了显著的进化。如今,程序员不再需要一遍遍地编写冗长的 if 判断,代码变得更加简洁、优雅,可读性也大幅提升。无论是通过 Optional 的链式调用,还是借助 Spring、Lombok 等现代化框架的工具类,Java 开发者都可以轻松实现高效、优雅的判空操作。接下来,我们将总结这些判空技巧,帮助你彻底告别繁琐的判空代码,提升开发效率与代码质量。
2025年03月18日
32 阅读
0 评论
0 点赞
2025-03-18
Spring 官宣接入 DeepSeek,太香了!
Spring AI已经支持DeepSeek。今天和大家聊聊如何在Spring Boot项目制使用DeepSeek,还是非常方便的!Spring AI介绍Spring AI是一个用于AI工程的应用程序框架,将Spring生态系统设计原则应用于AI领域。其核心是通过抽象化和模块化设计,简化AI功能的接入步骤,同时保持与Spring生态的无缝兼容。以下是其主要特点与功能:统一的抽象API:支持主流AI服务,如 OpenAI、DeepSeek、Google、和Ollama等,提供了提供标准化的接口。核心功能模块:模型交互、向量处理、检索增强生成(RAG)、函数调用。低代码集成:通过Spring Boot Starter依赖快速接入,在配置文件中配置好AI服务即可使用。结构化输出:将模型响应直接映射为Java对象,简化数据处理。流式响应:支持Flux流式输出,适用于实时聊天等场景。Spring AI集成DeepSeek申请Api Key首先我们需要去DeepSeek官网申请Api Key,地址: https://platform.deepseek.com/api_keys Spring Boot中集成DeepSeek首先在SpringBoot项目中添加Spring AI对应的依赖;<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-openai-spring-boot-starter</artifactId> <version>1.0.0-M6</version> </dependency>然后在项目的application.yml配置文件中添加调用AI服务相关的配置;spring: ai: openai: # 调用AI接口时表明身份的API KEY api-key: <YOUR_API_KEY> # 调用AI接口时的基础路径,配置的是阿里云百炼的基础路径 base-url: https://api.deepseek.com chat: options: # 调用的模型,DeepSeek的话可以选择deepseek-r1或deepseek-v3 model: deepseek-chat # 用来控制文本生成的随机性(创造力),值越小越严谨 temperature: 0.8创建一个控制器类,用于处理与 DeepSeek 的交互,import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/chat") public class ChatController { @Autowired private DeepSeekClient deepSeekClient; @PostMapping public String chat(@RequestBody String message) { return deepSeekClient.chatCompletion(message).getOutput().getContent(); } @GetMapping(value = "/stream", produces = "text/event-stream") public Flux<String> chatStream(@RequestParam String message) { return deepSeekClient.chatFluxCompletion(message) .map(response -> response.getOutput().getContent()); } }
2025年03月18日
22 阅读
0 评论
0 点赞
2024-07-06
Sprint Boot接入阿里通义千问
阿里通义千问是阿里巴巴推出的大规模语言模型,由达摩院研发。它是基于先进的自然语言处理技术构建的,旨在提供高质量的文本生成和理解能力。通义千问的特点包括:多语言支持:通义千问能够理解和生成多种语言的文本,包括但不限于中文、英文、日文、法文、西班牙文和德文等,这使得它具有全球化的交流能力。训练数据丰富:它的训练数据来自阿里巴巴内部的大量语言和文本资源,涵盖了文学、历史、科学、艺术等各种主题,旨在提供广泛的知识基础。应用场景广泛:通义千问不仅可以用于日常对话和信息查询,还可以为企业和个人用户提供定制化服务,如行业咨询、文档撰写、智能助手等,帮助用户生成内容或解答问题。与阿里巴巴产品整合:2023年4月,阿里巴巴宣布其所有产品将接入通义千问,这意味着用户可以在钉钉、天猫精灵等平台上直接体验到该模型的服务,企业也可以利用阿里云的能力来定制自己的行业专属大模型。合规性:2023年9月13日,通义千问通过了相关备案并正式对公众开放,表明其在遵守法律法规的前提下提供服务。商业价值:张勇(阿里巴巴集团董事会主席兼CEO)强调了通义千问对于提升阿里巴巴产品和服务的智能化水平,以及帮助企业利用人工智能进行创新。如果你是基于Python或Java开发,那么通义千问支持的SDK还是比较完善的,本文已Spring Boot接入阿里通义千问为例进行说明。壹、申请Key进入阿里云官网,定位到【API-KEY管理】,如果已经有Key的话,可以直接使用,如果没有可以创建一个新的。贰、创建Spring Boot工程具体怎么创建工程就不过多介绍了,现在主要说说创建完之后的配置及开发工作。2.1、添加依赖<dependency> <groupId>com.alibaba</groupId> <artifactId>dashscope-sdk-java</artifactId> <version>2.14.0</version> </dependency>2.2、修改配置文件在配置文件application.yml中添加我们申请到的Key注意替换成你实际的keyqwen: ai-api-key: sk-XXXX2.3、创建配置文件,读取配置信息@Component @ConfigurationProperties(prefix = "qwen") @Data public class QWenConfig { private String aiApiKey; }2.4、创建通义千问的配置文件@Configuration public class AliQWenConfig { @Bean public Generation generation() { return new Generation(); } }2.5、创建请求@RestController @RequestMapping("ai") public class QWenController { @Resource private Generation generation; @Resource private QWenConfig qWenConfig; /** * 测试demo * * @param content 用书输入文本内容 */ @PostMapping(value = "qwen") public String send(@RequestBody String content) throws NoApiKeyException, InputRequiredException { //用户与模型的对话历史。list中的每个元素形式为{“role”:角色, “content”: 内容}。 Message userMessage = Message.builder() .role(Role.USER.getValue()) .content(content) .build(); GenerationParam param = GenerationParam.builder() //指定用于对话的通义千问模型名 .model("qwen-turbo") .messages(Collections.singletonList(userMessage)) // .resultFormat(GenerationParam.ResultFormat.MESSAGE) //生成过程中核采样方法概率阈值,例如,取值为0.8时,仅保留概率加起来大于等于0.8的最可能token的最小集合作为候选集。 // 取值范围为(0,1.0),取值越大,生成的随机性越高;取值越低,生成的确定性越高。 .topP(0.8) //阿里云控制台DASHSCOPE获取的api-key .apiKey(qWenConfig.getAiApiKey()) //启用互联网搜索,模型会将搜索结果作为文本生成过程中的参考信息,但模型会基于其内部逻辑“自行判断”是否使用互联网搜索结果。 .enableSearch(true) .build(); GenerationResult generationResult = generation.call(param); return generationResult.getOutput().getChoices().get(0).getMessage().getContent(); } }叁、测试使用ApiFox测试一下
2024年07月06日
1,113 阅读
0 评论
0 点赞
2024-07-04
使用thumbnailator实现图片压缩
Thumbnailator是一个用于Java的强大的图片处理库,主要用于创建、缩放和裁剪图片的缩略图。它设计得既简单又功能强大,提供了一系列丰富的特性:尺寸调整:Thumbnailator能够根据指定的宽度或高度来调整图片大小。裁剪:可以将图片裁剪为特定的尺寸或长宽比。水印添加:可以在图片上添加文本或图片形式的水印。格式支持:支持多种图像格式,如JPEG、PNG、BMP、GIF等。Thumbnailator的使用非常直观。你只需要在你的项目中添加Thumbnailator的依赖(比如在Maven或Gradle的构建文件中),然后就可以在代码中调用其提供的方法了。下面结合项目示例,介绍一下使用Thumbnailator的方法,当然,关于添加水印、裁剪等功能你可以查看其github仓库https://github.com/coobird/thumbnailator壹、添加依赖我这里使用的0.4.20版本。 <!--thumbnailator图片处理--> <dependency> <groupId>net.coobird</groupId> <artifactId>thumbnailator</artifactId> <version>0.4.20</version> </dependency>贰、创建一个公共类这里简单介绍一下,getAccuracy是根据原图片大小,判断压缩的比例,压缩比例介于[0,1],比例越小,压缩的越小,当然相应的图片就会越模糊,所以这个比例可以根据实际情况进行调整,尽量保证压缩比例小的情况下,别造成图片失真。compressPicForScale方法,第一个参数代表源图片字节数组,第二个参数desFileSize代表要压缩到的大小,单位是kb,compressPicForScale方法对输入的图片循环压缩,直至压缩后文件大小<= desFileSizepackage cc.lisen.common.utils.file; import net.coobird.thumbnailator.Thumbnails; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; /** * description: 图片压缩 * * @author: leeframe * DateTime: 2024-07-04 11:54 */ public class PicUtils { //以下是常量 private static final Logger logger = LoggerFactory.getLogger(PicUtils.class); private static final Integer ZERO = 0; private static final Integer ONE_ZERO_TWO_FOUR = 1024; private static final Integer NINE_ZERO_ZERO = 900; private static final Integer THREE_TWO_SEVEN_FIVE = 3275; private static final Integer TWO_ZERO_FOUR_SEVEN = 2047; private static final Double ZERO_EIGHT_FIVE = 0.85; private static final Double ZERO_SEVEN_FIVE = 0.75; private static final Double ZERO_FOUR_FOUR = 0.44; private static final Double ZERO_FOUR = 0.4; /** * 根据指定大小压缩图片 * * @param imageBytes 源图片字节数组 * @param desFileSize 指定图片大小,单位kb * @return 压缩质量后的图片字节数组 */ public static byte[] compressPicForScale(byte[] imageBytes, long desFileSize) { if (imageBytes == null || imageBytes.length <= ZERO || imageBytes.length < desFileSize * ONE_ZERO_TWO_FOUR) { return imageBytes; } long srcSize = imageBytes.length; double accuracy = getAccuracy(srcSize / ONE_ZERO_TWO_FOUR); try { while (imageBytes.length > desFileSize * ONE_ZERO_TWO_FOUR) { ByteArrayInputStream inputStream = new ByteArrayInputStream(imageBytes); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(imageBytes.length); Thumbnails.of(inputStream) .scale(accuracy) .outputQuality(accuracy) .toOutputStream(outputStream); imageBytes = outputStream.toByteArray(); } logger.info("图片原大小={}kb | 压缩后大小={}kb", srcSize / ONE_ZERO_TWO_FOUR, imageBytes.length / ONE_ZERO_TWO_FOUR); } catch (Exception e) { logger.error("【图片压缩】msg=图片压缩失败!", e); } return imageBytes; } /** * 自动调节精度(经验数值) * * @param size 源图片大小 * @return 图片压缩质量比 */ private static double getAccuracy(long size) { double accuracy; if (size < NINE_ZERO_ZERO) { accuracy = ZERO_EIGHT_FIVE; } else if (size < TWO_ZERO_FOUR_SEVEN) { accuracy = ZERO_SEVEN_FIVE; } else if (size < THREE_TWO_SEVEN_FIVE) { accuracy = ZERO_FOUR_FOUR; } else { accuracy = ZERO_FOUR; } return accuracy; } } 叁、使用我这里是借助阿里云OSS,然后下载的网络图片/** * 阿里云文件上传(中保创) * * @param extension 文件后缀 * @param ownerDirectory 目录 */ public String upload(String extension, String ownerDirectory, String url) throws InvalidExtensionException, IOException { // 填写网络流地址。 try (InputStream inputStream = new URL(url).openStream()) { // 校验格式、大小等 String filePath = getFilePath(extension, ownerDirectory); byte[] bytesOriginal = IOUtils.toByteArray(inputStream); byte[] bytes = PicUtils.compressPicForScale(bytesOriginal, 400); ByteArrayInputStream inputStreamCompress = new ByteArrayInputStream(bytes); // 上传到阿里云 ossClient.putObject(aliyunConfig.getBucketName(), filePath, inputStreamCompress); //this.aliyunConfig.getUrlPrefix() + filePath 文件路径需要保持数据库 return aliyunConfig.getUrlPrefix() + filePath; } }
2024年07月04日
903 阅读
0 评论
0 点赞
2024-07-04
druid discard long time none received connection
阿里巴巴的Druid是一个Java数据库连接池(JDBC connection pool)组件,由阿里巴巴开发并开源。它不仅是一个数据库连接管理器,还提供数据源代理,SQL解析,监控等功能。Druid的主要特性包括:高效性:Druid使用了高效的连接池实现,减少了创建和销毁连接的开销。监控功能:Druid可以监控应用程序中的SQL执行情况,帮助开发者优化数据库操作。SQL解析:Druid能够解析SQL语句,对于一些复杂的SQL语句,可以进行优化或改写。防SQL注入:Druid通过SQL解析,能有效防止SQL注入攻击。高可用性:Druid支持主从读写分离,负载均衡等高级功能,提高系统的稳定性和性能。兼容性:Druid对主流的JDBC驱动和数据库都有很好的兼容性。当我们执行Sql时,如果当前执行时间与上一次执行Sql的时间间隔60s以上,在日志中就会有一条日志discard long time none received connection. , jdbcUrl : jdbc:mysql://xxx:3306/xxx?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC, version : 1.2.5, lastPacketReceivedIdleMillis : 62681这个信息,不影响程序正常运行,如果我们想屏蔽掉这个消息,可以在启动类中,加一个静态代码块 static { System.getProperties().put("druid.mysql.usePingMethod", "false"); }再次启动测试,间隔60s以上请求
2024年07月04日
953 阅读
0 评论
0 点赞
2024-07-03
Spring Boot动态修改刷新application.yaml文件
YAML(YAML Ain't Markup Language)是一种数据序列化格式,设计用于人类阅读和编写。它在功能上类似于JSON,但在表达复杂的数据结构时更为灵活和强大。YAML 的语法简洁明了,可以轻松地表示嵌套的列表和字典。以下是一些YAML的特点:易于阅读:YAML 的设计使其非常易于阅读和理解。例如,使用缩进而不是括号或大括号来表示层级关系,这使得YAML文件看起来更像自然语言。支持复杂的数据类型:YAML 可以表示复杂的嵌套数据结构,包括数组、字典和自定义类型。可扩展性:YAML 支持自定义标签,允许你定义自己的数据类型和表示形式。广泛的应用:YAML 被广泛用于配置文件,因为它的语法比其他格式如XML或JSON更容易编写和维护。此外,YAML 也常用于数据交换和存储。多语言支持:YAML 有多种编程语言的库支持,包括Python、Ruby、JavaScript等,这使得在不同环境中使用YAML变得容易。有时候我们可能在系统运行过程中,动态修改application.yaml的内容,并且在修改内容后,加载最新内容。为了修改application.yaml内容,我们可以借助snakeyaml进行修改,借助spring-cloud-context的ContextRefresher实现上下文的刷新。[alt type="warning"]借助ContextRefresher刷新有个限制。Spring Boot不能高于2.4.0版本,因为去掉了ConfigurationBeanFactoryMetadata,否则会提示下面的报错[/alt]java.lang.ClassNotFoundException: org.springframework.boot.context.properties.ConfigurationBeanFactoryMetadata壹、添加依赖添加依赖的时候,一定要注意版本,版本不兼容,会出现这个报错。<properties> <java.version>1.8</java.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <spring-boot.version>2.6.13</spring-boot.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.yaml</groupId> <artifactId>snakeyaml</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-context</artifactId> <version>2.1.2.RELEASE</version> </dependency> </dependencies>贰、增加配置文件用于测试application.yaml# 测试修改application.yaml application-modify: name: 测试叁、增加配置类,用于读取配置文件ApplicationModifyConfiguration.javapackage cc.lisen.application.modify.config; import lombok.Getter; import lombok.Setter; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; /** * description: * * @author: Laughing * DateTime: 2024-07-03 10:01 */ @Component @ConfigurationProperties(prefix = "application-modify") @Getter @Setter public class ApplicationModifyConfiguration { private String name; }肆、实际修改与读取配置文件代码package cc.lisen.application.modify.controller; import lombok.extern.slf4j.Slf4j; import cc.lisen.application.modify.config.ApplicationModifyConfiguration; import org.springframework.cloud.context.refresh.ContextRefresher; import org.springframework.util.ClassUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.yaml.snakeyaml.Yaml; import javax.annotation.Resource; import java.io.FileInputStream; import java.io.FileWriter; import java.io.IOException; import java.util.Map; import java.util.Objects; /** * description: * * @author: Laughing * DateTime: 2024-07-03 10:05 */ @RestController @RequestMapping("application/yaml") @Slf4j public class ApplicationModifyController { @Resource private ApplicationModifyConfiguration applicationModifyConfiguration; @Resource private ContextRefresher contextRefresher; @GetMapping("/modify") @SuppressWarnings({"unchecked"}) public void modifyApplicationYaml() throws IOException { String path = Objects.requireNonNull(Objects.requireNonNull(ClassUtils.getDefaultClassLoader()).getResource("application.yaml")).getPath(); Yaml yaml = new Yaml(); FileWriter fileWriter = null; try (FileInputStream fileInputStream = new FileInputStream(path)) { Map<String, Object> yamlMap = yaml.load(fileInputStream); Map<String,Object> applicationModifyMap = (Map<String, Object>) yamlMap.get("application-modify"); applicationModifyMap.put("name", "张三"); fileWriter = new FileWriter(path); fileWriter.write(yaml.dumpAsMap(yamlMap)); fileWriter.flush(); // 刷新配置文件 contextRefresher.refresh(); } catch (Exception exception) { log.error(exception.getLocalizedMessage()); } finally { log.info("finally"); if (fileWriter != null) { fileWriter.close(); } } } @GetMapping("/get") public String getApplicationYaml() { // 使用刷新后的配置 return applicationModifyConfiguration.getName(); } }修改完成后,可以请求测试一下
2024年07月03日
532 阅读
0 评论
0 点赞
2024-06-30
常见的后端性能优化方法
软件的响应速度和稳定性直接影响到用户的满意度和留存率。如果一个应用加载缓慢或频繁卡顿,用户可能会选择卸载并转向竞争对手的产品。良好的性能是保证用户界面流畅、操作响应快速的基础,有助于提升用户粘性和正面评价。本文不是教条式的指导,比如优化索引、重构代码等等这种形而上学的东西,而是重在动手实践,根据个人在日常开发中遇到的性能问题,通过具体的手段进行优化,比如加索引,到底索引加在哪,或者重构代码,应该在什么位置重构,代码应该怎么写。一、通过索引优化性能在数据库中加索引,能够优化查询、更新、删除的性能,但是索引并不是越多越好,这个还是需要根据实际情况的,举个例子,我们如果有如下的Sqlselect email,phone from user where name = '张三' 上面的sql很简单,查询姓名是张三的手机号、邮箱,因为我们需要通过name进行过滤,所以我们可以给name添加一个索引,避免触发全标扫描。二、留意隐式转换导致的性能问题隐式转换是指在进行查询、比较、计算或数据连接等操作时,如果涉及到的操作数具有不同的数据类型,数据库管理系统(DBMS)会自动将其中一个或多个操作数的数据类型转换为兼容的类型,以便这些操作可以顺利完成。这种转换过程是自动发生的,无需用户在查询语句中显式指定转换类型。这是一个在开发中不太容易犯错,但是一点出现问题极不容易发现的点。比如我们有一个表sys_dict_data其中有一个排序字段dict_sort,因为是排序字段,我们定义成int类型。如果我们代码里面写了下面一段Sqlselect dict_value, dict_label from sys_dict_data where dict_type = 'car_usage_nature' and dict_sort >= '3'这个Sql是完全正确的,但是有一个问题,我们在where条件里面写的是dict_sort >= '3',也就是字符串'3',这样就导致了一个隐式转换。所以我们应该正确写dict_sort >= 3三、避免大事务在Spring Boot开发中,我们经常使用@Transactional进行事务注解,因为注解是基于切面的,所以在方法开始就会开启事务,如果我们方法内部逻辑比较复杂,相对的事务就会比较长。举个不太恰当的栗子,我们有个需求是从数据库A表查询数据,查出来之后,对数据进行加工,加工之后,将数据插入到B表。数据查询方法比如是selectMethod()、数据计算方法是computeMethod()、数据插入方法是insertMethod(),如果我们有以下代码@Transactonal(rollbackFor = Exception.class) public void baseMethod(){ selectMethod(); computeMethod(); insertMethod(); }在selectMethod()方法中,因为我们只是查询数据,所以是不需要事务的,比如上面的方法,我们实际上就把事务给放大了。为了减小事务,我们可以把selectMethod()、computeMethod()放到事务外面。public void baseMethod(){ selectMethod(); computeMethod(); } @Transactonal(rollbackFor = Exception.class) public void insertMethod(){ }当然,因为事务是基于切面的,我们需要注意需要在外面调用insertMethod()避免事务失效。四、减少数据库查询数据这个如果是平时开发可能遇到的比较少,但是基于敏捷开发平台的遇到的会相对多点。比如我们现在的敏捷开发平台,平时创建表单时,会基于模型,但是因为表单涉及增删改查,所以模型上会有所有的表、字段。有时候与别的模块对接,为了省事,直接使用模型取数,但是对接过程中,我们只使用主表数据,子表取数就白白的消耗掉了性能。这种情况其实很好解决,我们在取数时,做到按需取数即可。五、减少重复取数这种情况,我觉得一般出现在取系统参数的过程中。在系统不断的迭代过程中,我们不断增加新的参数,如果增加一个参数就取一遍数据,也会导致大量的sql。针对这种情况,我们可以提取一个公共方法,将要获取的参数都放里面,尽量一次性取出来,并对参数进行缓存。避免重复从数据库取数。六、使用Redis缓存针对全局参数、一些配置参数等,因为改动很少,我们可以借助Redis对数据进行缓存。在获取参数时,先从Redis获取,如果获取到直接返回,如果获取不到,再从数据库获取,并将获取到的数据缓存到Redis中,在参数保存时,清掉Redis缓存的数据。七、合理使用order byorder by是一个成本很高的操作,如果非必要尽量不要使用order by,如果需要使用order by,尽可能在order by子句使用索引字段,减少排序的成本。八、减少使用临时表这里说的临时表,指的不仅仅是全局临时表也包括实表临时表(也就是一个表当临时表用),临时表如果使用不当,比如频繁地创建和销毁临时表,或者临时表设计不合理导致索引缺失,可能降低整体的数据库性能。另外,现在数据库种类很多,不同数据库对临时表的语法不尽相同,会增加程序移植的复杂度。涉及使用临时表的地方,可以考虑使用left join或inner join代替。九、关于exists关键字在我们的开发思维中,我们一般认为在大数据量时,exists的性能要比in的性能高,但是在国产神通数据库中,exists的性能却是特别的差(当然in也不高)。针对这种,我们可能需要针对特定数据库对sql进行优化,像神通数据库,我们可以改成left join并判断主键是否是null。十、关于or关键字当一个字段只有某几个值时,我们可能会这样查询select * from table where filed = '1' or filed = '2'针对这种,我们可以尝试使用union allselect * from table where filed = '1' union all select * from table where filed = '2'十一、串行改并行当函数串行时,整个计算过程耗时是所有函数耗时的和,改成并行后,理论上,耗时是最慢的一个函数的耗时。比如A调用B\C\D三个方法,其中B方法耗时1s,C方法耗时1.5s,D方法耗时2s,如果串行的话,不考虑A自身耗时,那么调用B\C\D的耗时为4.5s = 1s + 1.5s + 2s,如果我们改成并行,那么理论上耗时为 2s,也就是耗时最长的D方法的耗时。当然,如果涉及到多线程之间的同步,我们可以借助CountDownLatch等并发工具类,实现线程之间的同步。下面是一个简单的示例。@Slf4j public class CountDownLatchCase1 { public static void main(String[] args) throws InterruptedException { // 创建 CountDownLatch 对象,需要等待 3 个线程完成任务 CountDownLatch latch = new CountDownLatch(3); // 创建 3 个线程 Worker worker1 = new Worker(latch, "worker1"); Worker worker2 = new Worker(latch, "worker2"); Worker worker3 = new Worker(latch, "worker3"); // 启动 3 个线程 worker1.start(); worker2.start(); worker3.start(); // 等待 3 个线程完成任务 latch.await(); // 所有线程完成任务后,执行下面的代码 log.info("All workers have finished their jobs!"); } } class Worker extends Thread { private final CountDownLatch latch; public String name; public Worker(CountDownLatch latch, String name) { this.latch = latch; this.name = name; } @Override public void run() { try { // 模拟任务耗时 TimeUnit.MILLISECONDS.sleep(1000); log.info("{} has finished the job!", name); } catch (InterruptedException e) { log.error(e.getMessage(), e); } finally { // 一定要保证每个线程执行完毕或者异常后调用countDown()方法 // 如果不调用会导致其他线程一直等待, 无法继续执行 // 建议放在finally代码块中, 防止异常情况下未调用countDown()方法 latch.countDown(); } } }十二、分库分表当数据库中表的数据量过大时,可以考虑分库分表。比如每个月的数据量都比较大,我们考虑按月或者按年进行分表,每个月或者每年一张表。当然分库分表会增加后续查询的复杂度。十三、分页其实原本是不打算写这条的,因为分页是一个程序必备的东西,后来想想还是加上了,其实不管自己写Sql也好,各个ORM框架也都提供了分页的组件。
2024年06月30日
398 阅读
0 评论
0 点赞
2024-03-26
深度解析:如何在若依系统中集成阿里云OSS实现高效文件存储
零、引言随着信息化技术的快速发展,企业级应用对于海量文件存储的需求日益增长。而阿里云对象存储服务(OSS)以其高可用、高可靠、低成本的特点成为众多企业的首选解决方案。本文将以流行的开源后台管理系统——若依系统为例,详细阐述如何将其与阿里云OSS无缝集成,以实现文件资源的安全、高效存储。壹、若依系统上传文件的现状若依系统基于ElementUI的el-upload组件,对于我们的业务来讲,目前存在两个需要改进的地方(1)文件选择后会自动上传,这个在前面的文章有过介绍若依系统上传图片压缩 - 李森的博客 (llisen.cc)(2)若依系统上传文件是上传到应用服务器的,我们需要实现的是上传到阿里云OSS,同时可以将OSS内容,通过内网下载到ECS,方便备份文件,减少OSS存储费用。叁、开通并配置阿里云OSS首先,您需要在阿里云官网注册并登录账号,然后开通OSS服务。在控制台中创建一个新的Bucket,为您的项目设定专属的存储空间,并根据业务需求设置合适的访问权限和地域属性。获取Bucket的相关信息,包括Endpoint、AccessKey ID 和 AccessKey Secret,这是后续与OSS交互的重要凭证。肆、集成阿里云OSS SDK在若依系统的后端开发环境中,通过引入阿里云OSS SDK的依赖包:在根目录的pom.xml的properties配置阿里云OSS的版本 <properties> <aliyun-oss.version>3.17.4</aliyun-oss.version> </properties>在dependencyManagement配置阿里云 OSS依赖 <!--阿里云--> <dependency> <groupId>com.aliyun.oss</groupId> <artifactId>aliyun-sdk-oss</artifactId> <version>${aliyun-oss.version}</version> </dependency>接着,在项目的配置文件(若依是在admin工程的的resources文件夹中)中添加OSS相关的连接信息:Yaml# application.yml 示例 aliyun: endpoint: 'your-endpoint' endpointInternal: 'your-endpoint-internal' accessKeyId: 'your-access-key-id' accessKeySecret: 'your-access-key-secret' bucketName: 'your-bucket-name' urlPrefix: 'your-domain' urlPrefixInternal: 'https://' + 'your-endpoint-internal'解释一下上面几个配置的含义endpoint创建阿里云Bucket时提供的外网地域节点,使用这个endpoint实现文件的上传endpointInternal创建阿里云Bucket时提供的内网地域节点,为了节约费用,我们ECS跟OSS买的是同一个地域的,这样通过内网下载OSS的文件是不收取费用的,把文件通过内网备份到ECS后,我们可以在空闲的时候,将备份的文件,通过ECS下载到本地accessKeyId、accessKeySecret是您访问阿里云API的密钥,具有该账户完全的权限,这个可以在账户下的AccessKey管理查看bucketName这个是我们创建的Bucket名称伍、配置参数为了方便读取application.yml的配置参数,我们创建一个配置类并完成OSS初始化AliyunConfig.java@Configuration @ConfigurationProperties(prefix = "aliyun") @Data public class AliyunConfig { /** * 外网endpoint */ private String endpoint; /** * 内网endpoint */ private String endpointInternal; /** * key */ private String accessKeyId; /** * 密钥 */ private String accessKeySecret; /** * 空间名称 */ private String bucketName; /** * 外网Url前缀 */ private String urlPrefix; /** * 内网Url前缀 */ private String urlPrefixInternal; @Bean public OSS oSSClient() { return new OSSClient(endpoint, accessKeyId, accessKeySecret); } }陆、编写文件上传类第三步:编写文件上传逻辑在后端服务中创建一个专门处理文件上传的服务类或工具类,利用OSS SDK提供的API实现文件上传功能:AliyunFileUploadService.java@Component @Slf4j public class AliyunFileUploadService { /** * 默认大小 50M */ public static final long DEFAULT_MAX_SIZE = 50 * 1024 * 1024; @Resource private OSS ossClient; @Resource private AliyunConfig aliyunConfig; /** * 阿里云文件上传 * * @param file 上传的文件 * @param ownerDirectory 目录 */ public String upload(MultipartFile file, String ownerDirectory) throws InvalidExtensionException, IOException { //文件新路径 String originalFilename = file.getOriginalFilename(); // 校验格式、大小等 boolean isLegal = false; assertAllowed(file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION); String filePath = getFilePath(file, ownerDirectory); // 上传到阿里云 ossClient.putObject(aliyunConfig.getBucketName(), filePath, new ByteArrayInputStream(file.getBytes())); //this.aliyunConfig.getUrlPrefix() + filePath 文件路径需要保持数据库 return aliyunConfig.getUrlPrefix() + filePath; } /** * 生成文件路径 * * @param file 文件 * @param ownerDirectory 自定义目录 * @return 生成的文件目录 */ private String getFilePath(MultipartFile file, String ownerDirectory) { String fileName; String extension = getExtension(file); if (!StringUtils.isEmpty(ownerDirectory)) { fileName = ownerDirectory + "/" + IdUtils.fastUUID() + "." + extension; } else { fileName = DateUtils.datePath() + "/" + IdUtils.fastUUID() + "." + extension; } return fileName; } /** * 查看文件列表 * * @return 对象信息 */ public List<OSSObjectSummary> list() { // 设置最大个数。 final int maxKeys = 200; // 列举文件。 ObjectListing objectListing = ossClient.listObjects(new ListObjectsRequest(aliyunConfig.getBucketName()).withMaxKeys(maxKeys)); List<OSSObjectSummary> sums = objectListing.getObjectSummaries(); return sums; } /** * 删除文件 * * @param objectName 文件名 * @return 结果 */ public boolean delete(String objectName) { //如果文件路径是OSS的,截取后删除,否则不处理,直接返回成功 if (objectName != null && objectName.contains(aliyunConfig.getUrlPrefix())) { objectName = objectName.replace(aliyunConfig.getUrlPrefix(), ""); ossClient.deleteObject(aliyunConfig.getBucketName(), objectName); return true; } return true; } /** * 下载文件下载文件 * * @param objectName 数据库存储的文件路径 */ public void exportOssFile(String objectName) throws IOException { // ossObject包含文件所在的存储空间名称、文件名称、文件元信息以及一个输入流。 if (objectName != null && objectName.contains(aliyunConfig.getUrlPrefix())) { objectName = objectName.replace(aliyunConfig.getUrlPrefix(), ""); // 创建OSSClient实例。 OSS ossClientLocal = new OSSClientBuilder().build(aliyunConfig.getEndpointInternal(), aliyunConfig.getAccessKeyId(), aliyunConfig.getAccessKeySecret()); try { File file = getAbsoluteFile(objectName); ossClientLocal.getObject(new GetObjectRequest(aliyunConfig.getBucketName(), objectName), file); }catch (Exception exception){ throw new CustomException(exception.getMessage()); }finally { if (ossClientLocal != null) { ossClientLocal.shutdown(); } } } } /** * 文件大小校验 * * @param file 上传的文件 * @throws FileSizeLimitExceededException 如果超出最大大小 */ public void assertAllowed(MultipartFile file, String[] allowedExtension) throws FileSizeLimitExceededException, InvalidExtensionException { long size = file.getSize(); if (size > DEFAULT_MAX_SIZE) { throw new FileSizeLimitExceededException(DEFAULT_MAX_SIZE / 1024 / 1024); } String fileName = file.getOriginalFilename(); String extension = getExtension(file); if (allowedExtension != null && !isAllowedExtension(extension, allowedExtension)) { if (allowedExtension == MimeTypeUtils.IMAGE_EXTENSION) { throw new InvalidExtensionException.InvalidImageExtensionException(allowedExtension, extension, fileName); } else if (allowedExtension == MimeTypeUtils.FLASH_EXTENSION) { throw new InvalidExtensionException.InvalidFlashExtensionException(allowedExtension, extension, fileName); } else if (allowedExtension == MimeTypeUtils.MEDIA_EXTENSION) { throw new InvalidExtensionException.InvalidMediaExtensionException(allowedExtension, extension, fileName); } else { throw new InvalidExtensionException(allowedExtension, extension, fileName); } } } /** * 获取文件名的后缀 * * @param file 表单文件 * @return 后缀名 */ public String getExtension(MultipartFile file) { String extension = FilenameUtils.getExtension(file.getOriginalFilename()); if (StringUtils.isEmpty(extension)) { extension = MimeTypeUtils.getExtension(Objects.requireNonNull(file.getContentType())); } return extension; } /** * 判断MIME类型是否是允许的MIME类型 * * @param extension 扩展名 * @param allowedExtension 允许的扩展名 * @return true-允许;false-不允许 */ public boolean isAllowedExtension(String extension, String[] allowedExtension) { for (String str : allowedExtension) { if (str.equalsIgnoreCase(extension)) { return true; } } return false; } /** * 生成本地文件 * * @param fileName 文件名 * @return 文件 * @throws IOException 异常 */ private File getAbsoluteFile(String fileName) throws IOException { String uploadDir = LeeFrameConfig.getProfile(); File desc = new File(uploadDir + File.separator + fileName); if (!desc.getParentFile().exists()) { desc.getParentFile().mkdirs(); } if (!desc.exists()) { desc.createNewFile(); } return desc; } }柒、Controller public AjaxResult add(List<MultipartFile> imageFileList, @RequestParam("form") String form) throws IOException, InvalidExtensionException { }Controller通过MultipartFile接收前端传递的文件,然后调用服务层完成上传。捌、前端交互在若依系统的前端部分,当用户选择文件后,前端需将文件转换为二进制数据并通过Ajax或者其他HTTP请求方式发送给后端。后端接收到请求后,调用OSS服务进行文件上传并将返回的URL反馈至前端展示或保存至数据库。具体可以参考若依系统上传图片压缩 - 李森的博客 (lisen.cc)其他、安全与优化考量为了增强安全性,可以考虑使用STS临时访问凭证进行上传操作,防止关键密钥泄露。另外,如果希望提高文件访问速度,可以为Bucket开启CDN加速,并根据实际场景调整缓存策略。总结起来,通过上述步骤,我们成功实现了若依系统与阿里云OSS的集成,使得整个系统的文件存储和管理能力得到了显著提升。这一过程不仅展示了云存储服务的优势,也展现了若依系统良好的扩展性和兼容性,为企业级应用提供了更加灵活且高效的文件管理方案。
2024年03月26日
1,855 阅读
2 评论
0 点赞
2024-03-24
Spring Boot Controller调用某个方法报Service注入为null
最近为了部署方便,尝试将项目的依赖与配置文件分开进行打包,可以参考Spring Boot分开打包依赖及配置文件 - 李森的博客 (lisen.cc)项目部署之后,试了一下,没有报错,但是后面在用的时候,有一个接口始终报空指针,通过日志分析,是服务层没有注入导致的。接口通过@Resource注入的 @Resource private ICarQuotationPriceHistoryService carQuotationPriceHistoryService;首先,既然别的接口都不存在问题,那么可以断定出现问题不是我们分打开打包依赖导致的。其次,在Idea中直接运行时,接口也不报错,说明方法本身不存在问题(姑且这么说吧),检查了配置、包名等地方,都没有发现问题。既然问题出现在这个方法,那说明肯定是这个方法出现了问题,检查了方法的注解、参数等,也都没发现问题,就在检查方法属性的时候,突然发现问题了,这个方法没有public,其他方法都是有pubic的,方法加上public后,问题解决其实这个地方,只是粗心大意了,忘记写public了。我们都知道,当一个方法没有修饰符时,默认就是default,default通常称为默认访问模式。该模式下,只允许在同一个包中进行访问。这也就为什么我们在不拆分依赖的时候,接口能正常访问,当我们拆分依赖后,因为我们这个是一个单独的模块(依赖),这个接口就无法访问了。通过这件事,得到了两个教训:1.做事不可粗心大意,像controller的方法,记得加public修饰符。2.遇到事情不要被表象迷惑,比如这种注入是null的,我们一般首先想到的是包名、扫描配置、注解上出现问题,往往不会考虑方法修饰符出现问题了。
2024年03月24日
650 阅读
0 评论
0 点赞
2024-03-24
Spring Boot分开打包依赖及配置文件
壹、为何要分开打包依赖Spring Boot默认会将依赖全部打包到一个jar中,这样导致的问题就是我们的一个jar往往很大。加之平时我们分模块开发,往往修改很小的一个部分,就需要打包整个jar包,上传整个jar到服务器。比如我用阿里云服务器,3M的带宽,如果我不拆分开依赖,仅仅是上传jar都需要耗时接近1分钟的时间。当然这样也有一些其他问题,比如我这种多模块的项目,如果我们修改了其他模块(非启动类所在模块),那么我们需要记得将打包的jar要放到依赖对应的文件夹中。贰、为何要分开打包配置文件相对于分开打包依赖,其实配置文件才是更有必要打包的。Spring Boot配置文件默认包裹在jar包中的形式,一方面容易造成配置文件的覆盖,另一方面修改配置文件也相对比较麻烦。叁、如何拆分打包依赖及配置文件Spring Boot分开打包依赖及配置文件的方法也比较简单,我们只需要修改pom.xml文件即可。只需要注意一点就是,如果我们是多模块的项目,需要修改主工程的pom.xml文件。添加一些配置属性,方便修改 <properties> <!--依赖输出目录--> <output.dependence.file.path>../output/lib/</output.dependence.file.path> <!--manifest中lib配置路径--> <manifest.classpath.prefix>lib</manifest.classpath.prefix> <!--jar输出目录--> <output.jar.file.path>../output/</output.jar.file.path> <!--配置文件输出目录--> <output.resource.file.path>../output/config/</output.resource.file.path> </properties>我这里实现的效果是把所有的文件都放到项目顶级的output文件夹中,项目的jar放到output中,依赖放到lib文件夹中,配置文件放到config文件夹中然后我们修改打包插件 <build> <plugins> <!-- 打JAR包,不包含依赖文件;显式剔除配置文件 --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <configuration> <outputDirectory>${output.jar.file.path}</outputDirectory> <!-- 将配置文件排除在jar包 --> <excludes> <exclude>*.properties</exclude> <exclude>*.yml</exclude> <exclude>*.xml</exclude> <exclude>*.txt</exclude> </excludes> <archive> <!-- 生成的jar中,包含pom.xml和pom.properties这两个文件 --> <addMavenDescriptor>true</addMavenDescriptor> <!-- 生成MANIFEST.MF的设置 --> <manifest> <!--这个属性特别关键,如果没有这个属性,有时候我们引用的包maven库 下面可能会有多个包,并且只有一个是正确的, 其余的可能是带时间戳的,此时会在classpath下面把那个带时间戳的给添加上去,然后我们 在依赖打包的时候, 打的是正确的,所以两头会对不上,报错。 --> <useUniqueVersions>false</useUniqueVersions> <!-- 为依赖包添加路径, 这些路径会写在MANIFEST文件的Class-Path下 --> <addClasspath>true</addClasspath> <!-- MANIFEST.MF 中 Class-Path 各个依赖加入前缀 --> <!--这个jar所依赖的jar包添加classPath的时候的前缀,需要 下面maven-dependency-plugin插件补充--> <!--一定要找对目录,否则jar找不到依赖lib--> <classpathPrefix>${manifest.classpath.prefix}</classpathPrefix> <!--指定jar启动入口类 --> <mainClass>cc.lisen.LeeFrameApplication</mainClass> </manifest> <manifestEntries> <!-- 假如这个项目可能要引入一些外部资源,但是你打包的时候并不想把 这些资源文件打进包里面,这个时候你必须在 这边额外指定一些这些资源文件的路径,假如你的pom文件里面配置了 <scope>system</scope>,就是你依赖是你本地的 资源,这个时候使用这个插件,classPath里面是不会添加,所以你得手动把这个依赖添加进这个地方 --> <!--MANIFEST.MF 中 Class-Path 加入自定义路径,多个路径用空格隔开 --> <!--此处resources文件夹的内容,需要maven-resources-plugin插件补充上--> <Class-Path>${output.resource.file.path}</Class-Path> </manifestEntries> </archive> </configuration> </plugin> <!-- 复制依赖的jar包到指定的文件夹里 --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-dependency-plugin</artifactId> <executions> <execution> <id>copy-dependencies</id> <phase>package</phase> <goals> <goal>copy-dependencies</goal> </goals> <configuration> <!-- 拷贝项目依赖包到指定目录下 --> <outputDirectory>${output.dependence.file.path}</outputDirectory> <!-- 是否排除间接依赖,间接依赖也要拷贝 --> <excludeTransitive>false</excludeTransitive> <!-- 是否带上版本号 --> <stripVersion>false</stripVersion> </configuration> </execution> </executions> </plugin> <!-- 用于复制指定的文件 --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-resources-plugin</artifactId> <executions> <!-- 复制配置文件 --> <execution> <id>copy-resources</id> <phase>package</phase> <goals> <goal>copy-resources</goal> </goals> <configuration> <resources> <resource> <directory>src/main/resources</directory> <includes> <!--将如下格式配置文件拷贝--> <exclude>*.properties</exclude> <exclude>*.yml</exclude> <exclude>*.xml</exclude> <exclude>*.txt</exclude> </includes> </resource> </resources> <!--输出路径--> <outputDirectory>${output.resource.file.path}</outputDirectory> </configuration> </execution> </executions> </plugin> </plugins> <finalName>${project.artifactId}</finalName> </build>
2024年03月24日
1,004 阅读
0 评论
0 点赞
1
2
...
18