首页
归档
留言
广告合作
友链
美女主播
Search
1
博瑞GE车机升级/降级
5,146 阅读
2
Mac打印机设置黑白打印
4,517 阅读
3
修改elementUI中el-table树形结构图标
4,516 阅读
4
Mac客户端添加腾讯企业邮箱方法
4,351 阅读
5
intelliJ Idea 2022.2.X破解
4,060 阅读
Java
HarmonyOS Next
Web前端
微信开发
开发辅助
App开发
数据库
随笔日记
登录
/
注册
Search
标签搜索
Spring Boot
Java
Spring Cloud
Mac
mybatis
WordPress
Nacos
Spring Cloud Alibaba
Mybatis-Plus
jQuery
Java Script
asp.net
微信小程序
Sentinel
UniApp
MySQL
asp.net core
IntelliJ IDEA
Jpa
树莓派
Laughing
累计撰写
570
篇文章
累计收到
1,424
条评论
首页
栏目
Java
HarmonyOS Next
Web前端
微信开发
开发辅助
App开发
数据库
随笔日记
页面
归档
留言
广告合作
友链
美女主播
搜索到
211
篇与
的结果
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,064 阅读
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 net.xiangcaowuyu.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日
863 阅读
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日
911 阅读
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 net.xiangcaowuyu.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 net.xiangcaowuyu.application.modify.controller; import lombok.extern.slf4j.Slf4j; import net.xiangcaowuyu.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日
478 阅读
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日
377 阅读
0 评论
0 点赞
2024-03-26
深度解析:如何在若依系统中集成阿里云OSS实现高效文件存储
零、引言随着信息化技术的快速发展,企业级应用对于海量文件存储的需求日益增长。而阿里云对象存储服务(OSS)以其高可用、高可靠、低成本的特点成为众多企业的首选解决方案。本文将以流行的开源后台管理系统——若依系统为例,详细阐述如何将其与阿里云OSS无缝集成,以实现文件资源的安全、高效存储。壹、若依系统上传文件的现状若依系统基于ElementUI的el-upload组件,对于我们的业务来讲,目前存在两个需要改进的地方(1)文件选择后会自动上传,这个在前面的文章有过介绍若依系统上传图片压缩 - 香草物语 (xiangcaowuyu.net)(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反馈至前端展示或保存至数据库。具体可以参考若依系统上传图片压缩 - 香草物语 (xiangcaowuyu.net)其他、安全与优化考量为了增强安全性,可以考虑使用STS临时访问凭证进行上传操作,防止关键密钥泄露。另外,如果希望提高文件访问速度,可以为Bucket开启CDN加速,并根据实际场景调整缓存策略。总结起来,通过上述步骤,我们成功实现了若依系统与阿里云OSS的集成,使得整个系统的文件存储和管理能力得到了显著提升。这一过程不仅展示了云存储服务的优势,也展现了若依系统良好的扩展性和兼容性,为企业级应用提供了更加灵活且高效的文件管理方案。
2024年03月26日
1,683 阅读
2 评论
0 点赞
2024-03-24
Spring Boot Controller调用某个方法报Service注入为null
最近为了部署方便,尝试将项目的依赖与配置文件分开进行打包,可以参考Spring Boot分开打包依赖及配置文件 - 香草物语 (xiangcaowuyu.net)项目部署之后,试了一下,没有报错,但是后面在用的时候,有一个接口始终报空指针,通过日志分析,是服务层没有注入导致的。接口通过@Resource注入的 @Resource private ICarQuotationPriceHistoryService carQuotationPriceHistoryService;首先,既然别的接口都不存在问题,那么可以断定出现问题不是我们分打开打包依赖导致的。其次,在Idea中直接运行时,接口也不报错,说明方法本身不存在问题(姑且这么说吧),检查了配置、包名等地方,都没有发现问题。既然问题出现在这个方法,那说明肯定是这个方法出现了问题,检查了方法的注解、参数等,也都没发现问题,就在检查方法属性的时候,突然发现问题了,这个方法没有public,其他方法都是有pubic的,方法加上public后,问题解决其实这个地方,只是粗心大意了,忘记写public了。我们都知道,当一个方法没有修饰符时,默认就是default,default通常称为默认访问模式。该模式下,只允许在同一个包中进行访问。这也就为什么我们在不拆分依赖的时候,接口能正常访问,当我们拆分依赖后,因为我们这个是一个单独的模块(依赖),这个接口就无法访问了。通过这件事,得到了两个教训:1.做事不可粗心大意,像controller的方法,记得加public修饰符。2.遇到事情不要被表象迷惑,比如这种注入是null的,我们一般首先想到的是包名、扫描配置、注解上出现问题,往往不会考虑方法修饰符出现问题了。
2024年03月24日
605 阅读
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>net.xiangcaowuyu.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日
951 阅读
0 评论
0 点赞
2023-03-05
SprintBoot切面+Redis防止前端重复提交
最近项目上遇到重复提交的情况,虽然前端对按钮进行了禁用,但是不知道是什么原因,后端仍然接收到了多个请求,因为是分布式系统,所以不能简单的使用lock,最终考虑决定使用redis实现。一、环境准备MySql:测试数据库Redis:使用Redis实现Another Redis Desktop Manager:跟踪Redis信息ApiFox:模拟请求,单线程循环及多线程循环Spring Boot:2.7.4二、准备测试数据及接口2.1、创建表创建一个最简单的用户表,只包含id、name两列create table User ( id int null, name varchar(200) null );2.2、创建接口2.2.1、配置依赖及数据库、Redis连接信息项目依赖<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.4</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>net.xiangcaowuyu</groupId> <artifactId>RepeatSubmit</artifactId> <version>0.0.1-SNAPSHOT</version> <name>RepeatSubmit</name> <description>RepeatSubmit</description> <properties> <java.version>8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.2</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Druid --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.16</version> </dependency> <!--jdbc--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <!-- mysql --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!-- mybatis-plus --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.1</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project> yaml文件配置数据库及Redis连接信息spring: redis: host: 192.168.236.2 port: 6379 password: datasource: #使用阿里的Druid type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://192.168.236.2/TestRepeatSubmit?serverTimezone=UTC username: root password: root2.2.2、创建实体@Data @TableName("User") public class User { private Long id; private String name; }2.2.3、创建数据访问层public interface UserMapper extends BaseMapper<User> { }2.2.4、创建异常处理类@Data @NoArgsConstructor @AllArgsConstructor public class ResultRet<T> { private Integer code; private String msg; private T data; //成功码 public static final Integer SUCCESS_CODE = 200; //成功消息 public static final String SUCCESS_MSG = "SUCCESS"; //失败 public static final Integer ERROR_CODE = 201; public static final String ERROR_MSG = "系统异常,请联系管理员"; //没有权限的响应码 public static final Integer NO_AUTH_COOD = 999; //执行成功 public static <T> ResultRet<T> success(T data){ return new ResultRet<>(SUCCESS_CODE,SUCCESS_MSG,data); } //执行失败 public static <T> ResultRet failed(String msg){ msg = StringUtils.isEmpty(msg)? ERROR_MSG : msg; return new ResultRet(ERROR_CODE,msg,""); } //传入错误码的方法 public static <T> ResultRet failed(int code,String msg){ msg = StringUtils.isEmpty(msg)? ERROR_MSG : msg; return new ResultRet(code,msg,""); } //传入错误码的数据 public static <T> ResultRet failed(int code,String msg,T data){ msg = StringUtils.isEmpty(msg)? ERROR_MSG : msg; return new ResultRet(code,msg,data); } }2.2.5、简单的全局异常@Slf4j @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(value = Throwable.class) public ResultRet handleException(Throwable throwable){ log.error("错误",throwable); return ResultRet.failed(500, throwable.getCause().getMessage()); } }2.2.6、配置模拟接口模拟一个get请求的接口,用户新增用户,orm框架使用mybatis-plus,使用最简单的插入@RestController @RequestMapping("/user") public class UserController { @Resource private UserMapper userMapper; @GetMapping("/add") public ResultRet<User> add() { User user = new User(); user.setId(1L); user.setName("张三"); userMapper.insert(user); return ResultRet.success(user); } }以上配置完成后,当我们访问/user/add接口时,肯定访问几次,数据库就会重复插入多少信息。三、改造接口,防止重复提交改造的原理起始很简单,我们前端访问接口时,首先在头部都会携带token信息,我们通过切面,拦截请求,获取到token及请求的url,拼接后作为redis的key值,通过redis锁的方式写入key值,如果写入成功,设置一个过期时间,在有效期时间内,多次请求,先判断redis中是否有对应的key,如果有,抛出异常,禁止再次写入。3.1、配置RedisTemplate@Configuration public class RedisConfig { @Bean @SuppressWarnings(value = { "unchecked", "rawtypes" }) public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplate<Object, Object> template = new RedisTemplate<>(); template.setConnectionFactory(connectionFactory); Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class); // 使用StringRedisSerializer来序列化和反序列化redis的key值 template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(serializer); // Hash的key也采用StringRedisSerializer的序列化方式 template.setHashKeySerializer(new StringRedisSerializer()); template.setHashValueSerializer(serializer); template.afterPropertiesSet(); return template; } }3.2、增加Redis工具类@Component @Slf4j public class RedisUtils { @Resource private StringRedisTemplate stringRedisTemplate; /** * Redis分布式锁 * * @return 加锁成功返回true,否则返回false */ public boolean tryLock(String key, String value, long timeout) { Boolean isSuccess = stringRedisTemplate.opsForValue().setIfAbsent(key, value); //设置过期时间,防止死锁 if (Boolean.TRUE.equals(isSuccess)) { stringRedisTemplate.expire(key, timeout, TimeUnit.SECONDS); } return Boolean.TRUE.equals(isSuccess); } /** * Redis 分布式锁释放 * * @param key * @param value */ public void unLock(String key, String value) { try { String currentValue = stringRedisTemplate.opsForValue().get(key); if (StringUtils.isNotEmpty(currentValue) && StringUtils.equals(currentValue, value)) { stringRedisTemplate.opsForValue().getOperations().delete(key); } } catch (Exception e) { //这个是我的自定义异常,你可以删了 log.info("报错了"); } } }3.3、添加注解@Target(ElementType.METHOD) // 注解只能用于方法 @Retention(RetentionPolicy.RUNTIME) // 修饰注解的生命周期 @Documented public @interface RepeatSubmitAnnotation { /** * 防重复操作过期时间,默认1s */ long expireTime() default 1; }3.4、添加切面@Slf4j @Component @Aspect public class RepeatSubmitAspect { @Resource private RedisUtils redisUtils; /** * 定义切点 */ @Pointcut("@annotation(net.xiangcaowuyu.repeatsubmit.annotation.RepeatSubmitAnnotation)") public void repeatSubmit() { } @Around("repeatSubmit()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder .getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); Method method = ((MethodSignature) joinPoint.getSignature()).getMethod(); // 获取防重复提交注解 RepeatSubmitAnnotation annotation = method.getAnnotation(RepeatSubmitAnnotation.class); // 获取token当做key,小编这里是新后端项目获取不到哈,先写死 String token = request.getHeader("token"); if (StringUtils.isBlank(token)) { throw new RuntimeException("token不存在,请登录!"); } String url = request.getRequestURI(); /** * 通过前缀 + url + token 来生成redis上的 key * 可以在加上用户id,小编这里没办法获取,大家可以在项目中加上 */ String redisKey = "repeat_submit_key:" .concat(url) .concat(token); log.info("==========redisKey ====== {}", redisKey); boolean lock = redisUtils.tryLock(redisKey, redisKey, annotation.expireTime()); if (lock) { log.info("获取分布式锁成功"); try { //正常执行方法并返回 return joinPoint.proceed(); } catch (Throwable throwable) { throw new Throwable(throwable); } finally { //释放锁 // redisUtils.unLock(redisKey, redisKey); // System.out.println("释放分布式锁成功"); } } else { // 抛出异常 throw new Throwable("请勿重复提交"); } } }3.5、接口添加注解这里为了方便演示,我们把提交间隔时间设置为30s。@RestController @RequestMapping("/user") public class UserController { @Resource private UserMapper userMapper; @GetMapping("/add") @RepeatSubmitAnnotation(expireTime = 30L) public ResultRet<User> add() { User user = new User(); user.setId(1L); user.setName("张三"); userMapper.insert(user); return ResultRet.success(user); } }至此,我们所有的配置都完成了,接下来使用ApiFox模拟一下接口访问。3.6、模拟测试我们先把数据库及Redis清空(本来其实就是空的)配置好自动化测试接口3.6.1、单线程测试先模拟单线程操作,循环50次查看Redis,查看有一个key打开数据库,可以看到只成功插入了一条3.6.3、模拟多线程先把数据库清空,Redis等待过期后自动删除再次模拟,10个线程,循环10次此时查看数据库,仍然只有一条插入成功了
2023年03月05日
1,109 阅读
1 评论
0 点赞
2022-11-24
Spring Cloud Alibaba笔记修订版-第三章Nacos Discovery--服务治理
一、什么是服务治理服务治理是微服务架构中最核心最基本的模块,用于实现各个微服务的自动化注册与发现。服务注册:在服务治理框架中,都会构建一个注册中心,每个服务单元向注册中心登记自己提供的服务的详细信息。并在注册中心形成一张服务清单,服务注册中心需要以心跳的方式去监测清单中的服务是否可用,若不可用,需要再服务清单中剔除不可用的服务。服务发现:服务调用方向服务注册中心咨询服务,保宁获取所有服务的实例清单,实现对具体服务实例的访问。通过上面的图会发现,除了微服务,还有一个组件是服务注册中心,它是微服务架构中非常重要的一个组件,在微服务架构里起到了一个协调者的作用。注册中心一般包含以下几个功能:服务发现服务注册:保存服务提供者和服务调用者信息服务订阅:服务调用者订阅服务提供者的信息,注册中心向订阅者推送提供者信息服务配置配置订阅:服务提供者和服务调用者订阅微服务相关配置配置下发:主动将配置推送给服务提供者和服务调用者服务健康检测检测服务提供者的健康状况,如果发现异常,执行服务剔除常见的服务注册中心包括:Zookeeper、Eureka、Consul、Nacos。Nacos是Spring Cloud Alibaba组件之一,负责服务注册发现和服务配置,因为我们使用Spring Cloud Alibaba,所以这里只介绍Nacos的使用。二、Nacos简介Nacos致力于帮助您发现、配置和管理微服务。Nacos提供了一组简单易用的特性及,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。三、搭建Nacos环境注意使用Nacos之前,需要先配置好Java环境变量。我这里使用的服务器环境是Ubuntu 20.04,以下Nacos安装使用均以此为准,目前Nacos最新版本是2.1.2Nacos下载地址:Releases · alibaba/nacos (github.com)这里只介绍Nacos的基本使用,具体集群等高级用法,可以自行查找相关资料。3.1、下载Nacos下载nacos-server-2.1.2.tar.gz后,加压到任意位置。3.2、Nacos数据库文件运行Nacos之前,需要将Nacos数据库配置文件导入,我这里使用的是MySql,我直接导入上面Demo里面的数据库了。MySql的语句如下/* * Copyright 1999-2018 Alibaba Group Holding Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /******************************************/ /* 数据库全名 = nacos_config */ /* 表名称 = config_info */ /******************************************/ CREATE TABLE `config_info` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `data_id` varchar(255) NOT NULL COMMENT 'data_id', `group_id` varchar(255) DEFAULT NULL, `content` longtext NOT NULL COMMENT 'content', `md5` varchar(32) DEFAULT NULL COMMENT 'md5', `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', `src_user` text COMMENT 'source user', `src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip', `app_name` varchar(128) DEFAULT NULL, `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段', `c_desc` varchar(256) DEFAULT NULL, `c_use` varchar(64) DEFAULT NULL, `effect` varchar(64) DEFAULT NULL, `type` varchar(64) DEFAULT NULL, `c_schema` text, `encrypted_data_key` text NOT NULL COMMENT '秘钥', PRIMARY KEY (`id`), UNIQUE KEY `uk_configinfo_datagrouptenant` (`data_id`,`group_id`,`tenant_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info'; /******************************************/ /* 数据库全名 = nacos_config */ /* 表名称 = config_info_aggr */ /******************************************/ CREATE TABLE `config_info_aggr` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `data_id` varchar(255) NOT NULL COMMENT 'data_id', `group_id` varchar(255) NOT NULL COMMENT 'group_id', `datum_id` varchar(255) NOT NULL COMMENT 'datum_id', `content` longtext NOT NULL COMMENT '内容', `gmt_modified` datetime NOT NULL COMMENT '修改时间', `app_name` varchar(128) DEFAULT NULL, `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段', PRIMARY KEY (`id`), UNIQUE KEY `uk_configinfoaggr_datagrouptenantdatum` (`data_id`,`group_id`,`tenant_id`,`datum_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='增加租户字段'; /******************************************/ /* 数据库全名 = nacos_config */ /* 表名称 = config_info_beta */ /******************************************/ CREATE TABLE `config_info_beta` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `data_id` varchar(255) NOT NULL COMMENT 'data_id', `group_id` varchar(128) NOT NULL COMMENT 'group_id', `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name', `content` longtext NOT NULL COMMENT 'content', `beta_ips` varchar(1024) DEFAULT NULL COMMENT 'betaIps', `md5` varchar(32) DEFAULT NULL COMMENT 'md5', `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', `src_user` text COMMENT 'source user', `src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip', `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段', `encrypted_data_key` text NOT NULL COMMENT '秘钥', PRIMARY KEY (`id`), UNIQUE KEY `uk_configinfobeta_datagrouptenant` (`data_id`,`group_id`,`tenant_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_beta'; /******************************************/ /* 数据库全名 = nacos_config */ /* 表名称 = config_info_tag */ /******************************************/ CREATE TABLE `config_info_tag` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `data_id` varchar(255) NOT NULL COMMENT 'data_id', `group_id` varchar(128) NOT NULL COMMENT 'group_id', `tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id', `tag_id` varchar(128) NOT NULL COMMENT 'tag_id', `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name', `content` longtext NOT NULL COMMENT 'content', `md5` varchar(32) DEFAULT NULL COMMENT 'md5', `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', `src_user` text COMMENT 'source user', `src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip', PRIMARY KEY (`id`), UNIQUE KEY `uk_configinfotag_datagrouptenanttag` (`data_id`,`group_id`,`tenant_id`,`tag_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_tag'; /******************************************/ /* 数据库全名 = nacos_config */ /* 表名称 = config_tags_relation */ /******************************************/ CREATE TABLE `config_tags_relation` ( `id` bigint(20) NOT NULL COMMENT 'id', `tag_name` varchar(128) NOT NULL COMMENT 'tag_name', `tag_type` varchar(64) DEFAULT NULL COMMENT 'tag_type', `data_id` varchar(255) NOT NULL COMMENT 'data_id', `group_id` varchar(128) NOT NULL COMMENT 'group_id', `tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id', `nid` bigint(20) NOT NULL AUTO_INCREMENT, PRIMARY KEY (`nid`), UNIQUE KEY `uk_configtagrelation_configidtag` (`id`,`tag_name`,`tag_type`), KEY `idx_tenant_id` (`tenant_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_tag_relation'; /******************************************/ /* 数据库全名 = nacos_config */ /* 表名称 = group_capacity */ /******************************************/ CREATE TABLE `group_capacity` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', `group_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Group ID,空字符表示整个集群', `quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值', `usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量', `max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值', `max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数,,0表示使用默认值', `max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值', `max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量', `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_group_id` (`group_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='集群、各Group容量信息表'; /******************************************/ /* 数据库全名 = nacos_config */ /* 表名称 = his_config_info */ /******************************************/ CREATE TABLE `his_config_info` ( `id` bigint(20) unsigned NOT NULL, `nid` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `data_id` varchar(255) NOT NULL, `group_id` varchar(128) NOT NULL, `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name', `content` longtext NOT NULL, `md5` varchar(32) DEFAULT NULL, `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `src_user` text, `src_ip` varchar(50) DEFAULT NULL, `op_type` char(10) DEFAULT NULL, `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段', `encrypted_data_key` text NOT NULL COMMENT '秘钥', PRIMARY KEY (`nid`), KEY `idx_gmt_create` (`gmt_create`), KEY `idx_gmt_modified` (`gmt_modified`), KEY `idx_did` (`data_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='多租户改造'; /******************************************/ /* 数据库全名 = nacos_config */ /* 表名称 = tenant_capacity */ /******************************************/ CREATE TABLE `tenant_capacity` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', `tenant_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Tenant ID', `quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值', `usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量', `max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值', `max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数', `max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值', `max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量', `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_tenant_id` (`tenant_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='租户容量信息表'; CREATE TABLE `tenant_info` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `kp` varchar(128) NOT NULL COMMENT 'kp', `tenant_id` varchar(128) default '' COMMENT 'tenant_id', `tenant_name` varchar(128) default '' COMMENT 'tenant_name', `tenant_desc` varchar(256) DEFAULT NULL COMMENT 'tenant_desc', `create_source` varchar(32) DEFAULT NULL COMMENT 'create_source', `gmt_create` bigint(20) NOT NULL COMMENT '创建时间', `gmt_modified` bigint(20) NOT NULL COMMENT '修改时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_tenant_info_kptenantid` (`kp`,`tenant_id`), KEY `idx_tenant_id` (`tenant_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='tenant_info'; CREATE TABLE `users` ( `username` varchar(50) NOT NULL PRIMARY KEY, `password` varchar(500) NOT NULL, `enabled` boolean NOT NULL ); CREATE TABLE `roles` ( `username` varchar(50) NOT NULL, `role` varchar(50) NOT NULL, UNIQUE INDEX `idx_user_role` (`username` ASC, `role` ASC) USING BTREE ); CREATE TABLE `permissions` ( `role` varchar(50) NOT NULL, `resource` varchar(255) NOT NULL, `action` varchar(8) NOT NULL, UNIQUE INDEX `uk_role_permission` (`role`,`resource`,`action`) USING BTREE ); INSERT INTO users (username, password, enabled) VALUES ('nacos', '$2a$10$EuWPZHzz32dJN7jexM34MOeYirDdFAZm2kuWj7VEOJhhZkDrxfvUu', TRUE); INSERT INTO roles (username, role) VALUES ('nacos', 'ROLE_ADMIN');3.3、配置Nacos在conf文件夹下有一个application.properties,我们需要配置里面的数据库连接信息把大概34行往下的位置,取消注释并根据自己情况进行配置### If use MySQL as datasource: spring.datasource.platform=mysql ### Count of DB: db.num=1 ### Connect URL of DB: db.url.0=jdbc:mysql://127.0.0.1:3306/shop?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC db.user.0=root db.password.0=root3.4、运行Nacos进入bin文件夹执行./startup.sh -m standaloneNacos启动后,浏览器输入localhost:8848/nacos默认用户名及密码都是nacos四、将商品微服务注册到Nacos我们改造商品微服务,以便支持Nacos4.1、修改配置文件增加Nacos依赖<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> <version>2021.0.4.0</version> </dependency>4.2、主类上注解@EnableDiscoveryClient@SpringBootApplication @EntityScan({"net.xiangcaowuyu.shop.common.entity"}) @EnableDiscoveryClient public class ProductApplication { public static void main(String[] args) { SpringApplication.run(ProductApplication.class, args); } }4.3、配置文件添加nacos服务的地址spring: cloud: nacos: discovery: server-addr: 192.168.236.2:8848修改后配置文件如下server: port: 8081 spring: cloud: nacos: discovery: server-addr: 192.168.236.2:8848 application: name: service-user datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://192.168.236.2/shop?serverTimezone=UTC&characterEncoding=utf8&useUnicode=true&useSSL=true username: root password: root jpa: hibernate: #指定为update,每次启动项目检测表结构有变化的时候会新增字段,表不存在时会新建,如果指定create,则每次启动项目都会清空数据并删除表,再新建 ddl-auto: update naming: #指定jpa的自动表生成策略,驼峰自动映射为下划线格式 implicit-strategy: org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl # 默认false,在日志里显示执行的sql语句 show-sql: true properties: hibernate: dialect: org.hibernate.dialect.MySQL5Dialect database: mysql database-platform: org.hibernate.dialect.MySQL5Dialect4.4、查看服务是否注册成功重新启动product微服务服务名就是我们配置文件配置的应用名称。五、将订单微服务注册到Nacos我们改造订单微服务,以便支持Nacos5.1、修改配置文件增加Nacos依赖<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> <version>2021.0.4.0</version> </dependency>5.2、主类上注解@EnableDiscoveryClient@SpringBootApplication @EntityScan({"net.xiangcaowuyu.shop.common.entity"}) @EnableDiscoveryClient public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); } } 5.3、配置文件添加nacos服务的地址spring: cloud: nacos: discovery: server-addr: 192.168.236.2:8848修改后配置文件如下server: port: 8091 spring: cloud: nacos: discovery: server-addr: 192.168.236.2:8848 application: name: service-user datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://192.168.236.2/shop?serverTimezone=UTC&characterEncoding=utf8&useUnicode=true&useSSL=true username: root password: root jpa: hibernate: #指定为update,每次启动项目检测表结构有变化的时候会新增字段,表不存在时会新建,如果指定create,则每次启动项目都会清空数据并删除表,再新建 ddl-auto: update naming: #指定jpa的自动表生成策略,驼峰自动映射为下划线格式 implicit-strategy: org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl # 默认false,在日志里显示执行的sql语句 show-sql: true properties: hibernate: dialect: org.hibernate.dialect.MySQL5Dialect database: mysql database-platform: org.hibernate.dialect.MySQL5Dialect5.4、查看服务是否注册成功重新启动product微服务服务名就是我们配置文件配置的应用名称。5.5、改造订单接口,实现微服务调用package net.xiangcaowuyu.shop.order.controller; import lombok.extern.slf4j.Slf4j; import net.xiangcaowuyu.shop.common.entity.Product; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; import javax.annotation.Resource; import java.util.List; @RestController @RequestMapping("order") @Slf4j public class OrderController { @Resource private RestTemplate restTemplate; @Resource private DiscoveryClient discoveryClient; @GetMapping("product/{id}") public Product order(@PathVariable("id") Integer productID) { List<ServiceInstance> serviceInstanceLList = discoveryClient.getInstances("service-product"); log.info("获取到服务:" + serviceInstanceLList.size()); //忽略下面可能导致的错误 ServiceInstance serviceInstance = serviceInstanceLList.get(0); return restTemplate.getForObject(serviceInstance.getUri() + "/product/1", Product.class); } }浏览器访问订单接口六、负载均衡通俗的讲,负载均衡就是将负载(工作任务、访问请求)进行分摊到多个操作单元(服务器、组件)上进行执行。根据负载均衡发生的位置不同,一般分为服务端负载均衡和客户端负载均衡。服务端负载均衡指的是发生在服务提供者一方,比如nginx负载均衡客户端负载均衡指的是发生在服务请求一方,也就是在发送请求之前已经选好了由那个实例处理请求。微服务调用关系中一般会选择客户端负载均衡。6.1、准备负载均衡环境为了实现负载均衡,我们需要准备至少两个服务,这里以商品服务为例。在上面的代码中,我们pom.xml文件都没有加入打包的插件,为了启动两个服务,我们需要现将程序打包成jar包。在shop-user、shop-product、shop-order三个模块都加入打包插件。 <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>6.2、准备商品微服务为了区分调用的服务,我们在商品服务输出一下端口号。package net.xiangcaowuyu.shop.product.controller; import lombok.extern.slf4j.Slf4j; import net.xiangcaowuyu.shop.common.entity.Product; import net.xiangcaowuyu.shop.product.service.ProductService; import org.springframework.core.env.Environment; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; @RestController @Slf4j public class ProductController { @Resource private ProductService productService; @Resource private Environment environment; @GetMapping("/product/{id}") public Product product(@PathVariable("id") Integer ID) { log.error("当前端口号:" + environment.getProperty("local.server.port")); return productService.findByID(ID); } } 然后将shop-product打包成jar包,通过一下命令,启动两个服务java -jar shop-product-1.0-SNAPSHOT.jar --server.port=8081 java -jar shop-product-1.0-SNAPSHOT.jar --server.port=8082此时打开Nacos,可以看到shop-product有两个服务此时,我们运行订单工程,访问接口http://127.0.0.1:8091/order/product/1多次刷新之后,可以用看到服务全部都打到了8081端口上。8082一条都没有,可见并没有实现负载均衡。6.3、基于Ribbon实现负载均衡Ribbon是Spring Cloud的一个组件,它可以让我们使用一个注解就能轻松搞定负载均衡。nacos 2021版本已经没有自带Ribbon的整合,所以需要引入另一个支持的jar包 loadbalancer早shop-order中引入loadbalancer实现Ribbon支持<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-loadbalancer</artifactId> </dependency>在RestTemplate上注解上@LoadBalanced即可。package net.xiangcaowuyu.shop.order.config; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestTemplate; @Configuration public class OrderConfiguration { @LoadBalanced @Bean public RestTemplate getRestTemplate() { return new RestTemplate(); } } 改造接口,通过服务名访问package net.xiangcaowuyu.shop.order.controller; import lombok.extern.slf4j.Slf4j; import net.xiangcaowuyu.shop.common.entity.Product; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; import javax.annotation.Resource; import java.util.List; @RestController @RequestMapping("order") @Slf4j public class OrderController { @Resource RestTemplate restTemplate; @Resource DiscoveryClient discoveryClient; @GetMapping("product/{id}") public Product order(@PathVariable("id") Integer productID) { String serviceName = "service-product"; return restTemplate.getForObject("http://"+serviceName + "/product/1", Product.class); } }此时,我们运行订单工程,访问接口http://127.0.0.1:8091/order/product/1多次刷新之后,可以用看到服务平均打到了8081和8082端口。Ribbon默认的均衡策略是轮训。Ribbon自带的负载均衡策略我们可以通过修改配置文件改变默认的负载均衡策略。Ribbon 已经在最新的Spring Cloud 版本中被废弃,Spring Cloud Loadbalancer 是官方正式推出的一款新负载均衡利器,在未来,LoadBalancer 很有可能取代Ribbon的地位成为新一代的负载均衡器6.4、基于Feign实现负载均衡Feign是Spring Cloud提供的一个声明式的伪HTTP客户端,它使得调用远程服务就像调用本地服务一样简单,只需要创建一个接口并添加注解即可。Nacos很好的兼容了Feign,Feign默认集成了Ribbon,所以在Nacos下使用Feign默认就实现了负载均衡的效果。在进行一下代码之前,记得先移除6.3添加的Ribbon相关的代码我们改造shop-order工程,实现Feign的使用6.4.1、引入依赖<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-loadbalancer</artifactId> </dependency>6.4.2、在主类上添加Feign注解package net.xiangcaowuyu.shop.order; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.domain.EntityScan; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.openfeign.EnableFeignClients; @SpringBootApplication @EntityScan({"net.xiangcaowuyu.shop.common.entity"}) @EnableDiscoveryClient @EnableFeignClients public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); } } 6.4.3、添加一个servicepackage net.xiangcaowuyu.shop.order.Service; import net.xiangcaowuyu.shop.common.entity.Product; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @FeignClient("service-product") public interface ProductService { @GetMapping("/product/{id}") Product findByID(@PathVariable("id") Integer id); }Feign调用服务的地址就是@FeignClient+@GetMapping(获取其他映射)的地址6.4.4、改造Controllerpackage net.xiangcaowuyu.shop.order.controller; import lombok.extern.slf4j.Slf4j; import net.xiangcaowuyu.shop.common.entity.Product; import net.xiangcaowuyu.shop.order.Service.ProductService; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; import javax.annotation.Resource; import java.util.List; @RestController @RequestMapping("order") @Slf4j public class OrderController { @Resource ProductService productService; @GetMapping("product/{id}") public Product order(@PathVariable("id") Integer productID) { return productService.findByID(productID); } } 此时,我们运行订单工程,访问接口http://127.0.0.1:8091/order/product/1多次刷新之后,可以用看到服务平均打到了8081和8082端口。6.4.5、修改轮训策略以前的Ribbon有多种负载均衡策略但LoadBalancer貌似只提供了两种负载均衡器,不指定的时候默认用的是轮询。RandomLoadBalancer 随机RoundRobinLoadBalancer 轮询添加LoadBalance配置类package net.xiangcaowuyu.shop.order.config; import com.alibaba.cloud.nacos.NacosDiscoveryProperties; import com.alibaba.cloud.nacos.loadbalancer.NacosLoadBalancer; import org.springframework.cloud.loadbalancer.core.RandomLoadBalancer; import org.springframework.cloud.loadbalancer.core.ReactorServiceInstanceLoadBalancer; import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier; import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory; import org.springframework.context.annotation.Bean; import org.springframework.core.env.Environment; import javax.annotation.Resource; public class MyLoadBalancerConfig { @Resource private NacosDiscoveryProperties nacosDiscoveryProperties; //自定义loadBlancer负载均衡策略 @Bean public ReactorServiceInstanceLoadBalancer reactorServiceInstanceLoadBalancer(Environment environment, LoadBalancerClientFactory loadBalancerClientFactory) { String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME); //返回随机轮询负载均衡方式 return new RandomLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name); //返回加权随机轮询负载均衡方式 //return new RoundRobinLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name); //nacos服务注册中心权重的负载均衡策略 // return new NacosLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name, nacosDiscoveryProperties); } } 启动类上配置服务使用的负载均衡策略package net.xiangcaowuyu.shop.order; import net.xiangcaowuyu.shop.order.config.MyLoadBalancerConfig; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.domain.EntityScan; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient; import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients; import org.springframework.cloud.openfeign.EnableFeignClients; @SpringBootApplication @EntityScan({"net.xiangcaowuyu.shop.common.entity"}) @EnableDiscoveryClient @EnableFeignClients @LoadBalancerClients(value = @LoadBalancerClient(name = "service-product",configuration = MyLoadBalancerConfig.class)) public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); } }此时,我们运行订单工程,访问接口http://127.0.0.1:8091/order/product/1多次刷新之后,可以用看到服务随机打到了8081和8082端口。
2022年11月24日
762 阅读
0 评论
0 点赞
2022-11-23
Spring Cloud Alibaba笔记修订版-第二章微服务环境搭建
因为第一章都是一些概念性的东西,包括系统架构的演变、微服务架构的介绍(服务调度、服务治理、服务容错、链路追踪等等),大家感兴趣的可以阅读原文,我们这里直接从第二章微服务环境搭建开始。本次使用的电商项目中的商品、订单、用户为案例进行讲解。一、技术选型JDK :1.8maven :3.8.6数据库 :MySQL 8.0.31持久层 :SpringData Jpa其他 :Spring Cloud Alibaba 2021.0.4.0,截止到目前最新版本开发工具 :IntelliJ idea 2022.2# 二、模块设计springcloud-alibaba:父工程shop-common:公共模块【实体类】shop-user:用户微服务【端口:807x】shop-product:商品微服务【端口:808x】`shop-order:订单微服务【端口:809x】2.1、创建父工程打开idea,创建maven工程,选择【New Project】,输入Name、GroupId、ArtifactId,选择存储目录,JDK选择本机安装的1.8版本。点击CREATE完成项目创建。springcloud-alibaba只是作为父工程,我们不会写任何代码,所以直接把src文件夹整体删掉。然后在pom.xml中添加相关依赖<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.6.11</version> <relativePath/> </parent> <groupId>net.xiangcaowuyu</groupId> <artifactId>springcloud-alibaba</artifactId> <version>1.0-SNAPSHOT</version> <packaging>pom</packaging> <properties> <java.version>1.8</java.version> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <spring-cloud-alibaba.version>2021.0.4.0</spring-cloud-alibaba.version> <spring-cloud.version>2021.0.4</spring-cloud.version> </properties> <dependencyManagement> <dependencies> <!-- Spring Cloud依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> <!-- Spring Cloud Alibaba依赖--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>${spring-cloud.version}</version> </dependency> </dependencies> </dependencyManagement> </project>注意一下是Spring Cloud Alibaba与Spring Cloud及Spring Boot的版本对应关系,一定要选择对应的版本。Spring Cloud Alibaba VersionSpring Cloud VersionSpring Boot Version2021.0.4.0*Spring Cloud 2021.0.42.6.112021.0.1.0Spring Cloud 2021.0.12.6.32021.1Spring Cloud 2020.0.12.4.22.2、创建shop-common模块2.2.1、创建模块在工程上右键,选择NEW→Module名称输入shop-common创建模块2.2.2、添加依赖因为我们使用JPA,因此需要引入JPA的依赖,为了减少代码量,同时使用了lombok及fastjson序列化,所以shop-common添加依赖以下依赖 <dependencies> <!-- jpa依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <!-- lombok依赖--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <!-- fastjson依赖--> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>2.0.19</version> </dependency> <!-- MySQL依赖--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.31</version> </dependency> </dependencies>2.2.3、创建包目前shop-common模块中,src文件夹还是空的,为了规范,我们把shop-common的包定义为net.xiangcaowuyu.shop.common2.2.4、创建实体我们统一把实体放到entity下,创建三个实体:用户(User)、商品(Product)、订单(Order)package net.xiangcaowuyu.shop.common.entity; import lombok.Data; import javax.persistence.*; /** * 用户 */ @Data @Entity @Table(name = "shop_user") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer ID; /** * 用户名 */ private String name; /** * 密码 */ private String password; /** * 手机号码 */ private String telephone; } package net.xiangcaowuyu.shop.common.entity; import lombok.Data; import javax.persistence.*; /** * 商品 */ @Data @Entity @Table(name = "shop_product") public class Product { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer ID; /** * 商品名 */ private String name; /** * 价格 */ private Double price; /** * 库存 */ private Integer stock; } package net.xiangcaowuyu.shop.common.entity; import lombok.Data; import javax.persistence.*; /** * 订单 */ @Data @Entity @Table(name = "shop_order") public class Order { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer ID; /** * 用户ID */ private Integer uid; /** * 商品ID */ private Integer pid; } 2.3、创建用户微服务2.3.1、创建模块参考2.2.1创建用户模块shop-user2.3.2、添加依赖添加以下依赖即可。 <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>net.xiangcaowuyu</groupId> <artifactId>shop-common</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies>2.3.3、创建包目前shop-user模块中,src文件夹还是空的,为了规范,我们把shop-user的包定义为net.xiangcaowuyu.shop.user2.3.4、编写主类shop-user作为一个微服务,必须是可独立运行的,因此必须创建一个主类UserApplication.java。package net.xiangcaowuyu.shop.product; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @EntityScan({"net.xiangcaowuyu.shop.common.entity"}) public class UserApplication { public static void main(String[] args) { SpringApplication.run(UserApplication.class, args); } }2.3.5、创建配置文件在resources文件夹添加application.yaml配置文件server: port: 8071 spring: application: name: service-user datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://192.168.236.2/shop?serverTimezone=UTC&characterEncoding=utf8&useUnicode=true&useSSL=true username: root password: root jpa: hibernate: #指定为update,每次启动项目检测表结构有变化的时候会新增字段,表不存在时会新建,如果指定create,则每次启动项目都会清空数据并删除表,再新建 ddl-auto: update naming: #指定jpa的自动表生成策略,驼峰自动映射为下划线格式 implicit-strategy: org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl # 默认false,在日志里显示执行的sql语句 show-sql: true properties: hibernate: dialect: org.hibernate.dialect.MySQL5Dialect database: mysql database-platform: org.hibernate.dialect.MySQL5Dialect2.3.6、验证设置完成后,shop-user整个结构如下启用UserApplication,此时控制台输入一下内容,说明启动成功2.4、创建商品微服务2.4.1、创建模块参考2.2.1创建商品模块shop-product2.4.2、添加依赖添加以下依赖即可。 <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>net.xiangcaowuyu</groupId> <artifactId>shop-common</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies>2.4.3、创建包目前shop-product模块中,src文件夹还是空的,为了规范,我们把shop-product的包定义为net.xiangcaowuyu.shop.product2.4.4、编写主类shop-product作为一个微服务,必须是可独立运行的,因此必须创建一个主类ProductApplication.java。package net.xiangcaowuyu.shop.user; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @EntityScan({"net.xiangcaowuyu.shop.common.entity"}) public class ProductApplication { public static void main(String[] args) { SpringApplication.run(ProductApplication.class, args); } }2.4.5、创建配置文件在resources文件夹添加application.yaml配置文件server: port: 8081 spring: application: name: service-product datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://192.168.236.2/shop?serverTimezone=UTC&characterEncoding=utf8&useUnicode=true&useSSL=true username: root password: root jpa: hibernate: #指定为update,每次启动项目检测表结构有变化的时候会新增字段,表不存在时会新建,如果指定create,则每次启动项目都会清空数据并删除表,再新建 ddl-auto: update naming: #指定jpa的自动表生成策略,驼峰自动映射为下划线格式 implicit-strategy: org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl # 默认false,在日志里显示执行的sql语句 show-sql: true properties: hibernate: dialect: org.hibernate.dialect.MySQL5Dialect database: mysql database-platform: org.hibernate.dialect.MySQL5Dialect2.4.6、验证设置完成后,shop-product整个结构如下启用ProductApplication,此时控制台输入一下内容,说明启动成功2.5、创建订单微服务2.5.1、创建模块参考2.2.1创建商品模块shop-order2.5.2、添加依赖添加以下依赖即可。 <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>net.xiangcaowuyu</groupId> <artifactId>shop-common</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies>2.5.3、创建包目前shop-order模块中,src文件夹还是空的,为了规范,我们把shop-product的包定义为net.xiangcaowuyu.shop.order2.5.4、编写主类shop-order作为一个微服务,必须是可独立运行的,因此必须创建一个主类OrderApplication.java。package net.xiangcaowuyu.shop.order; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @EntityScan({"net.xiangcaowuyu.shop.common.entity"}) public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); } }2.5.5、创建配置文件在resources文件夹添加application.yaml配置文件server: port: 8091 spring: application: name: service-order datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://192.168.236.2/shop?serverTimezone=UTC&characterEncoding=utf8&useUnicode=true&useSSL=true username: root password: root jpa: hibernate: #指定为update,每次启动项目检测表结构有变化的时候会新增字段,表不存在时会新建,如果指定create,则每次启动项目都会清空数据并删除表,再新建 ddl-auto: update naming: #指定jpa的自动表生成策略,驼峰自动映射为下划线格式 implicit-strategy: org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl # 默认false,在日志里显示执行的sql语句 show-sql: true properties: hibernate: dialect: org.hibernate.dialect.MySQL5Dialect database: mysql database-platform: org.hibernate.dialect.MySQL5Dialect2.5.6、验证设置完成后,shop-order整个结构如下启用OrderApplication,此时控制台输入一下内容,说明启动成功三、原始服务调用3.1、创建测试数据为了方便后续演示,我们现在用户及商品表中创建几条测试数据。INSERT INTO shop_user (id, name, password, telephone) VALUES (1, '张三', '123456', '13333333333'); INSERT INTO shop_user (id, name, password, telephone) VALUES (2, '李四', '123456', '14444444444'); INSERT INTO shop_product (id, name, price, stock) VALUES (1, '小米', 1000, 5000); INSERT INTO shop_product (id, name, price, stock) VALUES (2, '华为', 2000, 5000); INSERT INTO shop_product (id, name, price, stock) VALUES (3, '苹果', 3000, 5000); INSERT INTO shop_product (id, name, price, stock) VALUES (4, '一加', 4000, 5000); 3.2、商品提供查询服务3.2.1、数据访问层创建商品数据访问层一个空的接口,JPA默认会提供查询方法package net.xiangcaowuyu.shop.product.dao; import net.xiangcaowuyu.shop.common.entity.Product; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; /** * 商品数据访问层 */ @Repository public interface ProductDao extends JpaRepository<Product, Integer> { } 3.2.2、服务层创建ProductService及接口实现类package net.xiangcaowuyu.shop.product.service; import net.xiangcaowuyu.shop.common.entity.Product; public interface ProductService { /** * 根据ID查询商品 * * @param ID 商品ID * @return 商品 */ Product findByID(Integer ID); } package net.xiangcaowuyu.shop.product.service.impl; import net.xiangcaowuyu.shop.common.entity.Product; import net.xiangcaowuyu.shop.product.dao.ProductDao; import net.xiangcaowuyu.shop.product.service.ProductService; import org.springframework.stereotype.Service; import javax.annotation.Resource; @Service public class ProductServiceImpl implements ProductService { @Resource private ProductDao productDao; /** * 根据ID查询商品 * * @param ID 商品ID * @return 商品 */ @Override public Product findByID(Integer ID) { return productDao.findById(ID).get(); } } 3.2.3、创建Rest接口package net.xiangcaowuyu.shop.product.controller; import lombok.extern.slf4j.Slf4j; import net.xiangcaowuyu.shop.common.entity.Product; import net.xiangcaowuyu.shop.product.service.ProductService; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; @RestController @Slf4j public class ProductController { @Resource private ProductService productService; @GetMapping("/product/{id}") public Product product(@PathVariable("id") Integer ID) { return productService.findByID(ID); } } 运行程序,现在浏览器测试一下3.3、订单查询商品3.3.1、创建配置文件我们这里通过RestTemplate访问商品,因此增加一个配置文件。package net.xiangcaowuyu.shop.order.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestTemplate; @Configuration public class OrderConfiguration { @Bean public RestTemplate getRestTemplate(){ return new RestTemplate(); } } 3.3.1、创建Rest接口这里简单模拟一下订单查询商品信息,只简单创建一个订单的controllerpackage net.xiangcaowuyu.shop.order.controller; import lombok.extern.slf4j.Slf4j; import net.xiangcaowuyu.shop.common.entity.Product; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; import javax.annotation.Resource; @RestController @RequestMapping("order") @Slf4j public class OrderController { @Resource private RestTemplate restTemplate; @GetMapping("product/{id}") public Product order(@PathVariable("id") Integer productID){ return restTemplate.getForObject("http://localhost:8081/product/1", Product.class); } } 简单测试一下订单接口接口也是能够正常访问的。四、传统服务调用的一些弊端通过上面的代码,我们虽然实现了接口的调用,但是我们把服务提供者的网络地址(ip、端口)等硬编码到了代码中,这种做法存在很多问题:一旦服务提供者地址发生变化,就需要手工修改代码(当然可以做成配置或者放到配置文件中)一旦是多个服务提供者,无法实现负载均衡功能一旦服务变得越来越多,人工维护调用关系变得非常困难为了解决上述问题,我们就引出了微服务架构中的服务治理,也就是通过注册中心等方式,实现服务的自动注册与发现。
2022年11月23日
875 阅读
0 评论
1 点赞
2022-11-22
Spring Cloud Alibaba笔记修订版-序言
一、为什么会有Spring Cloud Alibaba笔记修订版一系列的文章1、加强个人学习很无意间看到的“一本书”,之所以打上双引号,是一位这不是完整意义上的一本书,其实如标题说的一样,是一本笔记。【笔记】其实更像是个人学习的一个总结,所以书中内容可能会针对个人有深有浅,对于旁人来说,就是左一榔头、有一棒槌,云里雾里,不知所踪。为了个人的学习加深,也为了将别人的东西消化成自己的东西,因此在阅读这本书的时候,有意的做一下记录,形成一套完整的适合大多数人学习的Spring Cloud Alibaba笔记。2、完善代码书中错误【马虎】的代码比较多其实从一开始阅读,我就发发现笔记中有不少“错误”或者说是马虎的粘贴导致的错误,比如用户的微服务叫service-product,其实这是商品的微服务。笔记内代码相对比较古老,好多依赖都已经存在已知的漏洞Spring Cloud Alibaba笔记写的比较早,所以使用的Spring Cloud Alibaba还是比较早的版本,这并不是说是啥问题,只是随着时间的发展,新的版本替代老的版本是必然的趋势,因此我在阅读这个笔记的时候,特意验证了Spring Cloud Alibaba最新的版本,并且基于最新的版本进行代码的验证。二、Spring Cloud Alibaba笔记的改动点勘误其实就是对不合适或者马虎粘贴的代码进行一些改正。升级版本Spring Cloud Alibaba 升级到2021.0.1.0Spring Boot升级到2.6.3Spring Cloud升级到2021.0.1
2022年11月22日
937 阅读
0 评论
0 点赞
1
2
...
18