首页
归档
留言
广告合作
友链
美女主播
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开发
数据库
随笔日记
页面
归档
留言
广告合作
友链
美女主播
搜索到
89
篇与
的结果
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-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 点赞
2022-11-14
微信小程序获取用户头像后上传到七牛云
一、事情起因【油耗笔记OilNote】小程序好久没有升级了,最近打算对代码进行一些优化,但是新版本突然发现无法获取到用户微信头像及微信昵称了。查阅官方文档才知道,官方有对getUserProfile接口进行调整了。自 2022 年 10 月 25 日 24 时后(以下统称 “生效期” ),用户头像昵称获取规则将进行如下调整:自生效期起,小程序 wx.getUserProfile 接口将被收回:生效期后发布的小程序新版本,通过 wx.getUserProfile 接口获取用户头像将统一返回默认灰色头像,昵称将统一返回 “微信用户”。生效期前发布的小程序版本不受影响,但如果要进行版本更新则需要进行适配。自生效期起,插件通过 wx.getUserInfo 接口获取用户昵称头像将被收回:生效期后发布的插件新版本,通过 wx.getUserInfo 接口获取用户头像将统一返回默认灰色头像,昵称将统一返回 “微信用户”。生效期前发布的插件版本不受影响,但如果要进行版本更新则需要进行适配。通过 wx.login 与 wx.getUserInfo 接口获取 openId、unionId 能力不受影响。「头像昵称填写能力」支持获取用户头像昵称:如业务需获取用户头像昵称,可以使用「头像昵称填写能力」(基础库 2.21.2 版本开始支持,覆盖iOS与安卓微信 8.0.16 以上版本),具体实践可见下方《最佳实践》。小程序 wx.getUserProfile 与插件 wx.getUserInfo 接口兼容基础库 2.27.1 以下版本的头像昵称获取需求:对于来自低版本的基础库与微信客户端的访问,小程序通过 wx.getUserProfile 接口将正常返回用户头像昵称,插件通过 wx.getUserInfo 接口将正常返回用户头像昵称,开发者可继续使用以上能力做向下兼容。现在只要是发布的新版本,默认都需要调整,不然就显示下面灰色头像,已经发布的版本不受影响。既然官方调整了,那么我们也只有被动接受的份。二、油耗笔记的开发框架油耗笔记OilNote不是直接使用微信开发者工具开发的,而是使用UniApp开发的,后端是SpringBoot。三、改进思路3.1、获取用户头像由于官方指导意见是,使用button组件 open-type 的值设置为 chooseAvatar,当用户选择需要使用的头像之后,可以通过 bindchooseavatar 事件回调获取到头像信息的临时路径。从官方的指导我们可以看到,微信并没有给我们返回一个具体的路径,只是返回了一个临时的路径,因此我们就必须自己获取到这个临时的文件,然后存储起来。3.2、使用七牛云由于使用的腾讯云的低配服务器,带宽、存储都比较捉襟见肘,所以我打算把头像都存储到七牛云上,既能减轻带宽压力也能节省服务器空间。四、具体改进4.1、UniApp页面改进当用户通过微信登录时,此时获取用户头像信息,如果头像存在,登录之后跳转到首页,否则跳转到个人信息界面,让用户维护头像及昵称,此方法适用于新用户,同时也适用于老用户重新登录。//微信授权登录 getUserInfo(e) { let that = this; var p = this.getSetting(); p.then(function(isAuth) { console.log('是否已经授权', isAuth); if (isAuth) { console.log('用户信息,加密数据', e); //eData 包括//微信头像//微信名称 还有加密的数据. // let eData = JSON.parse(e.detail.rawData); uni.getUserProfile({ desc: 'Wexin', // 这个参数是必须的 success: function(infoRes) { //接下来就是访问接口. that.$request( 'wechat/authCode2Session?code=' + that.weChatCode, 'POST' ).then(function(res) { if (res.code == 200) { //将接口返回的数据保存在全局变量中. let userInfo = {} // 用户id userInfo.id = res.data.id userInfo.username = res.data.username userInfo.tel = res.data.tel userInfo.email = res.data.email userInfo.wechatOpenId = res.data.wechatOpenId userInfo.nickName = res.data.nickName userInfo.avatarUrl = res.data.avatarUrl ? res.data .avatarUrl : infoRes.userInfo.avatarUrl userInfo.gender = infoRes.userInfo.gender userInfo.password = '' if (!userInfo.province) { userInfo.province = uni.getStorageSync('province') } if (!userInfo.city) { userInfo.city = uni.getStorageSync('city') } uni.setStorageSync('userInfo', userInfo); if (!res.data.avatarUrl) { //没有头像时,跳转到用户信息维护界面 uni.redirectTo({ url: '/pages/profile/profile' }) } else { uni.redirectTo({ url: '/pages/index/index' }) } } }, function(err) { uni.showToast({ title: '授权登录失败!', mask: true, icon: 'none' }) } ) } }); } else { uni.showToast({ title: '授权失败,请确认授权已开启', mask: true, icon: 'none' }) } }); },4.1.1、登录界面改造4.1.2、个人信息界面改造油耗笔记之前有一个【个人信息】页面,因此我打算把修改头像的功能发放到里面。在合适的位置放入选择头像的按钮<button class="avatar-wrapper" open-type="chooseAvatar" @chooseavatar="onChooseAvatar"> <view class="cu-avatar xl round margin-center" :style="{backgroundImage:'url('+userInfo.avatarUrl+')'}"></view> </button>增加回调方法用户选择微信头像之后,会回调chooseavatar方法,因此我们增加一个chooseavatar用于用户选择头像之后上传到服务器(进一步上传到七牛)//选择头像回调 onChooseAvatar(e) { const that = this; this.$set(this.userInfo, "avatarUrl", e.detail.avatarUrl); uni.uploadFile({ url: operate.api + 'user/uploadAvatar/', //上传接口 header: { token: that.userInfo.id ? that.userInfo.id : '', }, formData: { 'userInfo': JSON.stringify(that.userInfo) }, filePath: e.detail.avatarUrl, name: 'file', success: (uploadFileRes) => { uni.hideLoading(); const back = JSON.parse(uploadFileRes.data); if (back.code == 200) { that.$set(that.userInfo, 'avatarUrl', back.data.avatarUrl) } else { uni.showToast(back.msg) } }, fail: (error) => { uni.hideLoading(); uni.showToast("图片上传失败,请联系开发!") }, complete: function() { uni.hideLoading(); } }); }4.2、后端改造我们首先在后端增加一个方法,用于接受前端传递的附件及其他参数(我这里主要传递的是用户信息,用户更新用户表,记录头像地址)。4.2.1、Controller增加接受用户上传头像的Api,具体的实现我们稍后在说。/** * 上传头像 * * @param multipartFile 文件信息 * @return 用户信息 */ @PostMapping("uploadAvatar") public AjaxResult uploadAvatar(@RequestParam("file") MultipartFile multipartFile,@RequestParam("userInfo") String oilUser) { return AjaxResult.success(oilUserService.uploadAvatar(multipartFile)); }4.2.2、增加七牛云依赖经过上面改造,我们已经可以将用户头像上传到我们的后端了,接下来的任务就是将头像上传到我们的七牛云了。现在pom.xml中增加七牛云的依赖<!-- 七牛云--> <dependency> <groupId>com.qiniu</groupId> <artifactId>qiniu-java-sdk</artifactId> <version>7.2.28</version> </dependency>4.2.3、增加七牛云配置为了方便使用,我们将七牛云的一些配置信息放入yaml文件中,方便维护。# ========================== ↓↓↓↓↓↓ 七牛云配置 ↓↓↓↓↓↓ ========================== qiniu: accessKey: XXX # Key secretKey: XXX # 密钥 bucket: XXX # 空间名称 domain: XXX # 访问域名 dir: XXX/ # 目录参数说明:accessKey:AK,在七牛云,个人中心,密钥管理中可以看到secretKey:SK,在七牛云,个人中心,密钥管理中可以看到bucket:空间名称,根据自己创建的空间填写domain:访问域名,根据控件绑定的域名实际填写dir:存储路径,因为七牛云默认是直接存储到根目录,为了方便管理,我们可以创建子目录,比如avatar,可以填写avatar/4.2.4、增加配置类为了方便使用,我们将yaml的值,映射到配置类上。/** * 七牛云实体 */ @Component @ConfigurationProperties(prefix = "qiniu") public class QiNiuConfig { /** * Key */ private static String accessKey; /** * 密钥 */ private static String secretKey; /** * 空间名称 */ private static String bucket; /** * 访问域名 */ private static String domain; /** * 目录 */ private static String dir; public static String getAccessKey() { return accessKey; } public void setAccessKey(String accessKey) { QiNiuConfig.accessKey = accessKey; } public static String getSecretKey() { return secretKey; } public void setSecretKey(String secretKey) { QiNiuConfig.secretKey = secretKey; } public static String getBucket() { return bucket; } public void setBucket(String bucket) { QiNiuConfig.bucket = bucket; } public static String getDomain() { return domain; } public void setDomain(String domain) { QiNiuConfig.domain = domain; } public static String getDir() { return dir; } public void setDir(String dir) { QiNiuConfig.dir = dir; } } 4.2.5、封装公共方法为了方便调用,我们将上传、删除等方法封装到单独的服务中。接口/** * 七牛接口 */ public interface IQiNiuService { /** * 以文件的形式上传 * * @param file * @param fileName: * @return: java.lang.String */ String uploadFile(File file, String fileName) throws QiniuException; /** * 以流的形式上传 * * @param inputStream * @param fileName: * @return: java.lang.String */ String uploadFile(InputStream inputStream, String fileName) throws QiniuException; /** * 删除文件 * * @param key: * @return: java.lang.String */ String delete(String key) throws QiniuException; } 实现@Service public class QiNiuServiceImpl implements IQiNiuService, InitializingBean { // 七牛文件上传管理器 private final Configuration cfg; private final Auth auth; public QiNiuServiceImpl() { // //构造一个带指定 Region 对象的配置类 cfg = new Configuration(Region.huadong()); auth = Auth.create(QiNiuConfig.getAccessKey(), QiNiuConfig.getSecretKey()); } /** * 定义七牛云上传的相关策略 */ private StringMap putPolicy; @Override public String uploadFile(File file, String fileName) throws QiniuException { if (!StringUtils.isEmpty(QiNiuConfig.getDir())) { fileName = QiNiuConfig.getDir() + fileName; } UploadManager uploadManager = new UploadManager(cfg); Response response = uploadManager.put(file, fileName, getUploadToken()); int retry = 0; while (response.needRetry() && retry < 3) { response = uploadManager.put(file, fileName, getUploadToken()); retry++; } if (response.statusCode == 200) { return "http://" + QiNiuConfig.getDomain() + "/" + fileName; } return "上传失败!"; } @Override public String uploadFile(InputStream inputStream, String fileName) throws QiniuException { if (!StringUtils.isEmpty(QiNiuConfig.getDir())) { fileName = QiNiuConfig.getDir() + fileName; } UploadManager uploadManager = new UploadManager(cfg); Response response = uploadManager.put(inputStream, fileName, getUploadToken(), null, null); int retry = 0; while (response.needRetry() && retry < 3) { response = uploadManager.put(inputStream, fileName, getUploadToken(), null, null); retry++; } if (response.statusCode == 200) { return "http://" + QiNiuConfig.getDomain() + "/" + fileName; } return "上传失败!"; } @Override public String delete(String key) throws QiniuException { BucketManager bucketManager = new BucketManager(auth, cfg); Response response = bucketManager.delete(QiNiuConfig.getBucket(), key); int retry = 0; while (response.needRetry() && retry++ < 3) { response = bucketManager.delete(QiNiuConfig.getBucket(), key); } return response.statusCode == 200 ? "删除成功!" : "删除失败!"; } @Override public void afterPropertiesSet() throws Exception { this.putPolicy = new StringMap(); putPolicy.put("insertOnly", 0); } /** * 获取上传凭证 */ private String getUploadToken() { return this.auth.uploadToken(QiNiuConfig.getBucket(), null, 3600, putPolicy); } } 有几个需要注意的点:在构造函数中,构造Configuration时,需要指定区域,因为我是华东区域的,因此使用的是Region.huadong(),如果使用的其他区域的,需要根据自己实际区域指定。在指定策略时,因为我一个用户只允许一个头像,因此上传时,如果存在我们直接覆盖的,所以在afterPropertiesSet方法中,设置上传策略时,直接指定的putPolicy.put("insertOnly", 0);,即如果存在就覆盖,如果不想覆盖,可以设置putPolicy.put("insertOnly", 1);,但是此时需要注意,如果上传重名文件,会返回异常。4.2.6、完善后用户上传头像方法用户上传头像后,更新用户实体(但是此时不更新数据库),将更新后的实体返回到前端,点击保存时,再更新数据库。 @Override public OilUser uploadAvatar(MultipartFile multipartFile, OilUser oilUser) throws IOException { String originalFilename = multipartFile.getOriginalFilename(); if (originalFilename == null || !originalFilename.contains(".")) { throw new CustomException("文件名不正确"); } String fileName = "avatar" + oilUser.getId() + originalFilename.substring(originalFilename.lastIndexOf(".")); String avatarUrl = qiNiuService.uploadFile(multipartFile.getInputStream(), fileName); oilUser.setAvatarUrl(avatarUrl); // LambdaUpdateWrapper<OilUser> userUpdateWrapper = new LambdaUpdateWrapper<>(); // userUpdateWrapper.set(OilUser::getAvatarUrl, oilUser.getAvatarUrl()); // userUpdateWrapper.eq(OilUser::getId, oilUser.getId()); // oilUserMapper.update(null, userUpdateWrapper); return oilUser; }4.2.7、用户保存方法分改造用户保存方法主要增加userUpdateWrapper.set(OilUser::getAvatarUrl, user.getAvatarUrl());,当用户有头像时,同步更新用户的头像信息。/** * 新增或保存用户 * * @param user 用户 * @return 结果 */ public OilUser saveUser(OilUser user) { if (user == null) { throw new CustomException("用户信息不能为空"); } if (user.getId() == null) { user.setId(""); } if (checkUserNameExist(user)) { throw new CustomException("用户名已存在"); } if (StringUtils.isEmpty(user.getId())) { user.setId(UUID.randomUUID().toString()); if (!StringUtils.isEmpty(user.getPassword())) { user.setPassword(passwordEncoder.encode(user.getPassword())); } oilUserMapper.insert(user); } else { LambdaUpdateWrapper<OilUser> userUpdateWrapper = new LambdaUpdateWrapper<>(); userUpdateWrapper.set(OilUser::getUsername, user.getUsername()); userUpdateWrapper.set(OilUser::getNickName, user.getNickName()); userUpdateWrapper.set(OilUser::getTel, user.getTel()); userUpdateWrapper.set(OilUser::getEmail, user.getEmail()); if (!StringUtils.isEmpty(user.getPassword())) { userUpdateWrapper.set(OilUser::getPassword, passwordEncoder.encode(user.getPassword())); } if (!StringUtils.isEmpty(user.getProvince())) { userUpdateWrapper.set(OilUser::getProvince, user.getProvince()); } if (!StringUtils.isEmpty(user.getCity())) { userUpdateWrapper.set(OilUser::getCity, user.getCity()); } if (!StringUtils.isEmpty(user.getAvatarUrl())) { userUpdateWrapper.set(OilUser::getAvatarUrl, user.getAvatarUrl()); } userUpdateWrapper.eq(OilUser::getId, user.getId()); oilUserMapper.update(null, userUpdateWrapper); user.setPassword(""); } return user; }五、效果微信用户登录后,如果没有上传过头像,会自动跳转到【个人信息】页面在个人信息上传头像后,自动跳转到首页。六、其他注意事项七牛云域名需要配置HTTPS小程序域名白名单uploadFile合法域名需要配置后台上传附件的域名。
2022年11月14日
1,995 阅读
0 评论
3 点赞
2022-11-06
Jackson反序列化时忽略某些属性的方法
在开发接口时,出于某些目的(比如有些字段我要设置默认值,不能受入参的影响),我们在入参字符串序列化成实体时,可能需要忽略某些属性。我们要达到的目的非常简单,一个用户类User.java,我们要达到的目的有两个:(1)控制序列化时,忽略name属性。(2)控制反序列化是,忽略name属性。一、@JsonIgnore注解当我们想控制是一个实体在序列化时,忽略某些属性,我们第一个想到的也许就是@JsonIgnore注解了,@JsonIgnore注解能控制实体在序列化时,忽略某些属性,但是无法控制实体在反序列化时忽略的属性。先来看一下我们测试用的User.java类。public class User implements Serializable { private String code; @JsonIgnore private String name; public String getCode() { return code; } public void setCode(String code) { this.code = code; } public String getName() { return name; } public void setName(String name) { this.name = name; } }1.1、序列化我们先试一下序列化,看看name属性能否在序列化时自动忽略掉。测试方法也非常简单ObjectMapper objectMapper = new ObjectMapper(); User user = new User(); user.setCode("code123"); user.setName("name123"); System.out.println(objectMapper.writeValueAsString(user));可以看到,name属性确实忽略了,也就是这个注解针对序列化是生效的。1.2、反序列化序列化时候没问题,那么反序列化时候是否也没问题呢,我们接着往下看。String userStr = "{\"code\":\"code123\",\"name\":\"name123\"}"; User user1 = objectMapper.readValue(userStr, User.class);这个时候我们不能在序列化输出显示了,我们调试看一下。我们可以看到,也是能够正常忽略掉的。但是,今天在项目开发时,我使用了同样的方法,发现@JsonIgnore注解没有起作用,但是实体类是这么样的@Data @NoArgsConstructor public class User implements Serializable { private String code; @JsonProperty(value = "name") @JsonIgnore private String name; @JsonProperty(value = "billCateProp") @JsonIgnore @JSONField(deserialize = false) private String billCateProp; @JsonFormat(pattern = "yyyy-MM-dd") private Date date; }怀疑了Data注解,也怀疑了@JsonProperty与@JsonIgnore注解冲突,甚至是属性Camel命名等,但是同样的代码,我拿到别的地方也有没有再现问题,所以终究也不知道是什么原因导致的。2、@JsonProperty@JsonIgnore的问题没有解决,只能另辟蹊径,最终发现@JsonIgnore注解有个access属性,通过access属性解决了问题,在反序列化时,不会再读取忽略的字段,遂将代码调整如下 @JsonProperty(value = "billCateProp",access = JsonProperty.Access.READ_ONLY) // @JsonIgnore @JSONField(deserialize = false) private String billCateProp;JsonProperty.Access.READ_ONLY代表反序列化是忽略字段。JsonProperty.Access.WRITE_ONLY代表序列化时忽略字段。@JsonIgnore失效的问题,最终也没找到答案,有知道的童鞋不妨留言说明一下。
2022年11月06日
1,490 阅读
0 评论
1 点赞
2022-05-21
SpringBoot事务提交后执行异步代码
一般情况下,我们在使用事务时,都是在方法上添加一个@Transactional注解 @Transactional(rollbackFor = Exception.class) public void test1() { }但是有些时候,除了主要核心业务外,我们可能还需要推送消息,但是推送消息我们又需要使用我们核心业务的数据,比如我在核心业务代码中执行了插入,之后需要异步获取插入的数据,推送消息或者发送给异构系统,这个时候,我们可以使用Spring Boot提供的TransactionSynchronization接口,并实现afterCommit方法package com.example.demo.service; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionSynchronizationAdapter; import org.springframework.transaction.support.TransactionSynchronizationManager; import javax.annotation.Resource; @Service public class Test123Service { @Resource Test2Service test2Service; @Transactional(rollbackFor = Exception.class) public void test1() { boolean synchronizationActive = TransactionSynchronizationManager.isSynchronizationActive(); if (synchronizationActive) { TransactionSynchronizationManager.registerSynchronization( new TransactionSynchronizationAdapter() { @Override public void afterCommit() { test2Service.test2(); } } ); } else { test2Service.test2(); } } } package com.example.demo.service; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; @Service public class Test2Service { @Async public void test2(){ System.out.println("test2"); } }注意:方法test1()不能调用同一给类中使用了@Async注解的方法test2()(此时@Async会失效)
2022年05月21日
2,649 阅读
1 评论
1 点赞
2022-02-12
spring boot主流办公文档的在线预览源码
kkFileView此项目为文件文档在线预览项目解决方案,对标业内付费产品有【永中office】【office365】【idocv】等,在取得公司高层同意后以Apache协议开源出来反哺社区,在此特别感谢@唐老大的支持以及@端木详笑的贡献。该项目使用流行的spring boot搭建,易上手和部署,基本支持主流办公文档的在线预览,如doc,docx,Excel,pdf,txt,zip,rar,图片等等项目特性支持 office, pdf, cad 等办公文档支持 txt, xml(渲染), md(渲染), java, php, py, js, css 等所有纯文本支持 zip, rar, jar, tar, gzip 等压缩包支持 jpg, jpeg, png, gif, tif, tiff 等图片预览(翻转,缩放,镜像)使用 spring-boot 开发,预览服务搭建部署非常简便rest 接口提供服务,跨语言、跨平台特性(java,php,python,go,php,....)都支持,应用接入简单方便抽象预览服务接口,方便二次开发,非常方便添加其他类型文件预览支持最最重要 Apache 协议开源,代码 pull 下来想干嘛就干嘛官网及文档地址:https://kkfileview.keking.cn在线体验请善待公共服务,会不定时停用地址:https://file.keking.cn项目文档(Project documentation)详细wiki文档:https://gitee.com/kekingcn/file-online-preview/wikis/pages中文文档:https://gitee.com/kekingcn/file-online-preview/blob/master/README.mdEnglish document:https://gitee.com/kekingcn/file-online-preview/blob/master/README.en.md联系我们,加入组织我们会用心回答解决大家在项目使用中的问题,也请大家在提问前至少 Google 或 baidu 过,珍爱生命远离无效的交流沟通QQ群号:613025121(已满) 2群:484680571文档预览效果1. 文本预览支持所有类型的文本文档预览, 由于文本文档类型过多,无法全部枚举,默认开启的类型如下 txt,html,htm,asp,jsp,xml,json,properties,md,gitignore,log,java,py,c,cpp,sql,sh,bat,m,bas,prg,cmd 文本预览效果如下 2. 图片预览支持jpg,jpeg,png,gif等图片预览(翻转,缩放,镜像),预览效果如下 3. word文档预览支持doc,docx文档预览,word预览有两种模式:一种是每页word转为图片预览,另一种是整个word文档转成pdf,再预览pdf。两种模式的适用场景如下图片预览:word文件大,前台加载整个pdf过慢pdf预览:内网访问,加载pdf快图片预览模式预览效果如下 pdf预览模式预览效果如下 4. ppt文档预览支持ppt,pptx文档预览,和word文档一样,有两种预览模式 图片预览模式预览效果如下 pdf预览模式预览效果如下 5. pdf文档预览支持pdf文档预览,和word文档一样,有两种预览模式 图片预览模式预览效果如下 pdf预览模式预览效果如下 6. excel文档预览支持xls,xlsx文档预览,预览效果如下 7. 压缩文件预览支持zip,rar,jar,tar,gzip等压缩包,预览效果如下 可点击压缩包中的文件名,直接预览文件,预览效果如下 8. 多媒体文件预览理论上支持所有的视频、音频文件,由于无法枚举所有文件格式,默认开启的类型如下 mp3,wav,mp4,flv 视频预览效果如下 音频预览效果如下 9. CAD文档预览支持CAD dwg文档预览,和word文档一样,有两种预览模式 图片预览模式预览效果如下 pdf预览模式预览效果如下 考虑说明篇幅原因,就不贴其他格式文件的预览效果了,感兴趣的可以参考下面的实例搭建下快速开始项目使用技术spring boot: spring boot开发参考指南freemarkerredissonjodconverter依赖外部环境redis (可选,默认不用)OpenOffice 或者 LibreOffice( Windows 下已内置,Linux 脚本启动模式会自动安装,Mac OS 下需要手动安装)第一步:pull 项目 https://github.com/kekingcn/file-online-preview.git第二步:运行 ServerMain 的 main 方法,服务启动后,访问 http://localhost:8012/会看到如下界面,代表服务启动成功历史更新记录2021年7月6日,v4.0.0 版本发布 :底层集成OpenOffice替换为LibreOffice,Office文件兼容性增强,预览效果提升修复压缩文件目录穿越漏洞修复PPT预览使用PDF模式无效修复PPT图片预览模式前端显示异常新增功能:首页文件上传功能可通过配置实时开启或禁用优化增加Office进程关闭日志优化Windows环境下,查找Office组件逻辑(内置的LibreOffice优先)优化启动Office进程改同步执行2021年6月17日,v3.6.0 版本发布 :ofd 类型文件支持版本,本次版本重要功能均由社区开发贡献,感谢 @gaoxingzaq、@zhangxiaoxiao9527 的代码贡献新增 ofd 类型文件预览支持,ofd 是国产的类似 pdf 格式的文件新增了 ffmpeg 视频文件转码预览支持,打开转码功能后,理论上支持所有主流视频的预览,如 rm、rmvb、flv 等美化了 ppt、pptx 类型文件预览效果,比之前版本好看太多更新了 pdfbox、xstream、common-io 等依赖的版本2021年1月28日 :2020农历年最后一个版本发布,主要包含了部分 UI 改进,和解决了 QQ 群友、 Issue 里反馈的 Bug 修复,最最重要的是发个新版,过个好年引入galimatias,解决不规范文件名导致文件下载异常更新index接入演示界面UI风格更新markdown文件预览UI风格更新XML文件预览UI风格,调整类文本预览架构,更方便扩展更新simTxT文件预览UI风格调整多图连续预览上下翻图的UI采用apache-common-io包简化所有的文件下载io操作XML文件预览支持切换纯文本模式增强url base64解码失败时的提示信息修复导包错误以及图片预览 bug修复发行包运行时找不到日志目录的问题修复压缩包内多图连续预览的bug修复大小写文件类型后缀没通用匹配的问题指定Base64转码采用Apache Commons-code中的实现,修复base64部分jdk版本下出现的异常修复类文本类型HTML文件预览的bug修复:dwg文件预览时无法在jpg和pdf两种类型之间切换escaping of dangerous characters to prevent reflected xss修复重复编码导致文档转图片预览失败的问题&编码规范2020年12月27日 :2020年年终大版本更新,架构全面设计,代码全面重构,代码质量全面提升,二次开发更便捷,欢迎拉源码品鉴,提issue、pr共同建设架构模块调整,大量的代码重构,代码质量提升N个等级,欢迎品鉴增强XML文件预览效果,新增XML文档数结构预览新增markdown文件预览支持,预览支持md渲染和源文本切换支持切换底层web server为jetty,解决这个issue:https://github.com/kekingcn/kkFileView/issues/168引入cpdetector,解决文件编码识别问题url采用base64+urlencode双编码,彻底解决各种奇葩文件名预览问题新增配置项office.preview.switch.disabled,控制offic文件预览切换开关优化文本类型文件预览逻辑,采用Base64传输内容,避免预览时再次请求文件内容office预览图片模式禁用图片放大效果,达到图片和pdf预览效果一致的体验直接代码静态设置pdfbox兼容低版本jdk,在IDEA中运行也不会有警告提示移除guava、hutool等非必须的工具包,减少代码体积Office组件加载异步化,提速应用启动速度最快到5秒内合理设置预览消费队列的线程数修复压缩包里文件再次预览失败的bug修复图片预览的bug2020年05月20日 :新增支持全局水印,并支持通过参数动态改变水印内容新增支持CAD文件预览新增base.url配置,支持使用nginx反向代理和使用context-path支持所有配置项支持从环境变量里读取,方便Docker镜像部署和集群中大规模使用支持配置限信任站点(只能预览来自信任点的文件源),保护预览服务不被滥用支持配置自定义缓存清理时间(cron表达式)全部能识别的纯文本直接预览,不用再转跳下载,如.md .java .py等支持配置限制转换后的PDF文件下载优化maven打包配置,解决 .sh 脚本可能出现换行符问题将前端所有CDN依赖放到本地,方便没有外网连接的用户使用首页评论服务由搜狐畅言切换到Gitalk修复url中包含特殊字符可能会引起的预览异常修复转换文件队列addTask异常修复其他已经问题官网建设:https://kkfileview.keking.cn官方Docker镜像仓库建设:https://hub.docker.com/r/keking/kkfileview2019年06月18日 :支持自动清理缓存及预览文件支持http/https下载流url文件预览支持FTP url文件预览加入Docker构建2019年04月08日 :缓存及队列实现抽象,提供JDK和REDIS两种实现(REDIS成为可选依赖)打包方式提供zip和tar.gz包,并提供一键启动脚本2018年01月19日 :大文件入队提前处理新增addTask文件转换入队接口采用redis队列,支持kkFIleView接口和异构系统入队两种方式2018年01月17日 :优化项目结构,抽象文件预览接口,更方便的加入更多的文件类型预览支持,方便二次开发新增英文文档说明(@幻幻Fate,@汝辉)贡献新增图片预览文件支持类型修复压缩包内轮播图片总是从第一张开始的问题2018年01月12日 :新增多图片同时预览支持压缩包内图片轮番预览2018年01月02日 :修复txt等文本编码问题导致预览乱码修复项目模块依赖引入不到的问题新增spring boot profile,支持多环境配置引入pdf.js预览doc等文件,支持doc标题生成pdf预览菜单,支持手机端预览使用登记如果这个项目解决了你的实际问题,可在 https://gitee.com/kekingcn/file-online-preview/issues/IGSBV登记下,如果节省了你的三方预览服务费用,也愿意支持下的话,可点击下方【捐助】请作者喝杯咖啡,也是非常感谢Stars 趋势图GiteeGitHub
2022年02月12日
1,052 阅读
0 评论
0 点赞
1
2
...
8