Spring Cloud
注:本章内容承接 spring boot / spring cloud遇到的一系列问题记录(一) —— 努力成为优秀的架构师
由于数据库字段有限,特此进行拆分。 完整源码参考 https://github.com/ShyZhen/scd
搭建配置中心 spring-cloud-config-server
目前我们的项目是微服务架构,如果每个项目都有自己的配置文件,首先管理起来麻烦,其次不够高端,于是需要搭建一个统一管理配置的服务,也就是我们的config模块。
- (1)在config模块的pom文件中引入依赖
<!-- 在config模块中引入spring-cloud-config-server依赖,搭建一个配置服务器 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
- (2)在启动类标注
@EnableConfigServer
注解
/scd/config/src/main/java/com/litblc/config/ConfigApplication.java
// 标注@EnableConfigServer搭建配置服务器
@EnableConfigServer
@SpringBootApplication
public class ConfigApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigApplication.class, args);
}
}
- (3)配置文件设置本地文件读取配置,也可以设置其他方式,比如github读取、数据库读取等
代码语言:javascript复制更多配置参考文档 https://springdoc.cn/spring-cloud-config/
# 配置服务器的端口,通常设置为8888:
server:
port: ${APP_PORT:8888}
spring:
application:
name: config-server
profiles:
# 从本地文件读取配置时,Config Server激活的profile必须设定为native:
active: native
cloud:
config:
server:
# 禁用 JdbcEnvironmentRepository 的自动配置。
jdbc:
enabled: false
native:
# 设置配置文件的搜索路径:
search-locations: file:./config-repo, file:../config-repo, file:../../config-repo
# 参考 `步骤(7)`,可以删掉这段配置项
# 依赖库中有spring-boot-starter-jdbc,必须配置本项目的DataSource
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/fmock
username: root
password: root
- (4)配置本地配置文件目录
在上面
search-locations
中我们配置了config-repo
文件夹,根据我们项目名字的不同,创建几个配置文件(文件名和项目名要对应上),代码结构如下
一些示例配置源码如下:
/scd/config-repo/application.yml
# 通用common configuration
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/fmock
username: root
password: root
data:
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
#password:
database: ${REDIS_DATABASE:0}
database1: ${REDIS_DATABASE1:1}
storage:
local:
# 文件存储根目录:
root-dir: ${STORAGE_LOCAL_ROOT:/var/storage}
max-size: ${STORAGE_LOCAL_MAX_SIZE:102400}
allow-empty: false
allow-types: jpg, png, gif
mybatis-plus:
mapper-locations: classpath:/mapper/*.xml
configuration:
map-underscore-to-camel-case: true
/scd/config-repo/fmock.yml
# fmock configuration
# http://localhost:8888/fmock/default
server:
port: ${APP_PORT:8081}
servlet:
context-path: /api
/scd/config-repo/push.yml
# push configuration
# http://localhost:8888/push/default
server:
port: ${APP_PORT:8083}
servlet:
context-path: /api
- (5)让其他项目使用配置中心的服务
完成上一步我们已经配置好了配置中心,启动项目输入http://localhost:8888/fmock/default
即可看到生效的配置,application.yml的配置是会一起返回的(访问地址跟文件名对应)
接下来我们要做的是让我们的fmock模块、push模块使用上配置中心服务
首先需要在fmock/pom.xml
添加客户端依赖,否则无法解析本地配置的import: configserver:xxx
参数
<!-- 使用配置中心,需要依赖SpringCloud Config客户端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
然后修改模块的fmock/src/main/resources/application.yml
配置文件
spring:
application:
# 必须设置app名称,要跟config-repo中的`文件名`对应:
name: ${APP_NAME:fmock}
config:
# 导入Config Server地址:
import: configserver:${CONFIG_SERVER:http://localhost:8888}
push/src/main/resources/application.yml
spring:
application:
# 必须设置app名称,要跟config-repo中的`文件名`对应:
name: ${APP_NAME:push}
config:
# 导入Config Server地址:
import: configserver:${CONFIG_SERVER:http://localhost:8888}
- (6)启动fmock或者push项目即可自动匹配
- (7)想区分环境,只需要更改模块的名字,并添加一个对应的配置文件即可
比如添加config-repo/fmock-dev.yml
开发环境配置文件,然后更改fmock模块的spring.application.name=fmock-dev
即可。
- (8)优化补充
我们在(3)
步骤中存在依赖库中有spring-boot-starter-jdbc,必须配置本项目的DataSource
的配置,
这个jdbc并不是spring-cloud-config-server
引起,因为引入的是可选<optional>true</optional>
,
实际上问题是之前我们的parent模块中统一引入了mybatis-plus依赖,导致间接引入了jdbc依赖。
正确处理方案是删除parent中的mybatis-plus依赖,放在dependencyManagement
中,其他模块需要的时候单独引入。
这样就可以删除配置中心配置了无用的datasource字段问题。
如何调用其他模块的服务、方法等
总结:直接引用调用是不行的,毕竟不是一个jar包,想要访问其他模块的服务,只能通过http请求,使用类似openfeign的包;common模块或者其他模块能使用,是因为它就是单独的代码,并没有启动类,没启动服务所以,所以没有进入spring容器也无法使用注解,也不涉及IP和端口之类的。
参数接收@PathVariable
、@RequestParam
、@RequestBody
的使用
@PathVariable
是path路径参数,在路由中直接体现出来@RequestParam
是url参数,一般形如?xx=1&xx=2
@RequestBody
是请求体参数,也就是postman中的raw格式
这里我们重点介绍@RequestBody
,在使用他之前,必须定义raw的参数结构。
这里我们在common模块中创建一个bean:
/common/src/main/java/com/litblc/common/requestBean/test/TestRaw.java
package com.litblc.common.requestBean.test;
public class TestRaw {
public long id;
public long userId;
public String nickname;
public String avatar;
}
然后在使用该bean的模块中引入common依赖(我是在push模块)
代码语言:javascript复制 <!-- 内部模块引入 -->
<dependency>
<groupId>com.litblc</groupId>
<artifactId>common</artifactId>
</dependency>
控制器使用示例
代码语言:javascript复制 @Operation(summary = "三种接受参数测试")
@PostMapping(value = "/raw/{path_id}/{sort_type}")
public TestRaw raw(
@PathVariable(value = "path_id") @Parameter(description = "path参数可以多个") long pathId,
@PathVariable(value = "sort_type") @Parameter(description = "path参数") String sort_type,
@RequestParam(value = "page", required = false, defaultValue = "1") @Parameter(description = "url参数可以多个") long page,
@RequestParam(value = "page_size", required = false, defaultValue = "15") @Parameter(description = "url参数") long pageSize,
@RequestBody TestRaw testRaw
) {
System.out.println(pathId);
System.out.println(sort_type);
System.out.println(page);
System.out.println(pageSize);
System.out.println(testRaw.nickname);
return testRaw;
}
优化文档knife4j
我们从一开始使用的是springboot推荐的默认文档包springdoc-openapi-starter-webmvc-ui
,这个包里集成了swagger-ui
,但是用着不太方便,于是这里我们尝试换成knife4j。
- 版本疑惑
我们在老项目中经常看到knife4j-spring-boot-starter
或者knife4j-openapi2-spring-boot-starter
这两个包,是因为该项目使用的是springboot2。
我们目前使用的是springboot3,需要使用knife4j-openapi3-jakarta-spring-boot-starter
这个包。
参考官网 https://doc.xiaominfo.com/docs/quick-start/start-knife4j-version#21-spring-boot-2x
我们也可以从源码上看到一些端倪:
knife4j-spring-boot-starter
引用的是旧版knife4j
,其中properties
规定java版本1.8
;
knife4j-openapi2-spring-boot-starter
和knife4j-openapi3-jakarta-spring-boot-starter
虽然都引入的最新版knife4j
,
默认的java<knife4j-java.version>1.8</knife4j-java.version>
也是1.8
,
但是,后者覆盖了properties
中的<knife4j-java.version>17</knife4j-java.version>
版本为17。
感兴趣的朋友可以自己查看,这里不放图了。
- 单个springboot项目使用
首先引入依赖knife4j-openapi3-jakarta-spring-boot-starter
<!-- 接口文档 swagger UI 本地访问 http://ip:port/swagger-ui/index.html-->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.1.0</version>
</dependency>
<!-- 接口文档 knife4j测试 http://ip:port/doc.html-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>4.3.0</version>
</dependency>
现在其实就可以使用了,现在访问http://ip:port/doc.html
即可访问knife4j的优化文档。
注:我们曾经引入过
springdoc-openapi-starter-webmvc-ui
依赖,访问http://ip:port/swagger-ui/index.html
依然可以用默认的swagger。
- 最后是可选的自定义配置
单个项目使用不需要配置,使用默认的即可,如果需要其他配置可以参考官网:
增强特性https://doc.xiaominfo.com/docs/features/enhance
代码语言:javascript复制# http://ip:port/swagger-ui/index.html
springdoc:
swagger-ui:
path: /swagger-ui.html
tags-sorter: alpha
operations-sorter: alpha
api-docs:
path: /v3/api-docs
group-configs:
- group: 'springbootstudy'
paths-to-match: '/**'
packages-to-scan: com.example.springbootstudy # packages-to-scan 默认为启动类所在的路径
- group: 'project2'
paths-to-match: '/**'
packages-to-scan: com.example.springbootstudy
# knife4j的增强配置,继承springdoc的配置 http://ip:port/doc.html
knife4j:
enable: true
setting:
language: ZH_CN # EN
单个springboot项目多个模块的文档配置
网上都是springboot2然后写配置类的文档,都一个样,生气,还得靠自己多思考多尝试。
在上面的“自定义配置”中,我们有个配置group-configs
并没有具体说明,参数packages-to-scan
就是扫描的模块地址。
- 有时候工程量比较大的时候会配置多个模块,用一个启动类管理,比如如下代码架构(主要是启动类提出来):
- 然后更新配置文件,主要是设置
group-configs
:
# http://ip:port/swagger-ui/index.html
springdoc:
swagger-ui:
path: /swagger-ui.html
tags-sorter: alpha
operations-sorter: alpha
api-docs:
path: /v3/api-docs
group-configs:
- group: 'springbootstudy模块'
paths-to-match: '/**'
packages-to-scan: com.example.springbootstudy # packages-to-scan 默认为启动类所在的路径
- group: 'push模块'
paths-to-match: '/**'
packages-to-scan: com.example.push
# knife4j的增强配置,继承springdoc的配置 http://ip:port/doc.html
knife4j:
enable: true
setting:
language: ZH_CN # EN
- 然后访问 http://ip:port/doc.html 和 http://ip:port/swagger-ui/index.html 发现已经成功搭建分组文档
代码语言:javascript复制题外话:这里我们加了一个push模块,可以测试
@ComponentScan
注解的使用
// 设置了这个就只扫描push包了,srpingbootstudy包就不会加载了,任何springbootstudy中的方法都不会生效
// @ComponentScan(value = "com.example.push")
@SpringBootApplication
public class SpringBootStudyApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootStudyApplication.class, args);
}
}
spring cloud 配置knife4j
使用缓存@EnableCaching
和 @Cacheable
参考https://springdoc.cn/spring-cache-redis-json/
spring的Spring Cache包可以设置多种缓存模式,我们使用redis的方式。注意前提是配置好redis能用。
- springboot依赖是
spring-boot-starter-cache
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
- 在配置文件中进行配置cache
spring:
# 缓存设置
cache:
type: redis
redis:
# 缓存有效的时间,默认永久有效,默认单位为毫秒,如60000=1m
time-to-live: 5m
# 如果指定了前缀就用我们指定的前缀,如果没有就默认使用缓存的名字作为前缀
key-prefix: "fmock:"
use-key-prefix: true
# 是否缓存空值,防止缓存穿透
# cache-null-values: true
- 现在就能使用了,只不过存的不是json,不方便阅读,需要自己设置序列化方式为 JSON
package com.litblc.fmock.moduleA.config;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.util.StringUtils;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
/**
* 使`@Cacheable`操作存储的数据自动格式化为json,而不是默认的二进制
* 注意与RedisTemplateConfig不要混淆,都要进行处理
*
* @Author zhenhuaixiu
* @Date 2023/11/6 14:34
* @Version 1.0
*/
@Configuration
public class CacheConfig {
@Bean
public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
// 先载入配置文件中的配置信息
CacheProperties.Redis redisProperties = cacheProperties.getRedis();
// 根据配置文件中的定义,初始化 Redis Cache 配
if (redisProperties.getTimeToLive() != null) {
redisCacheConfiguration = redisCacheConfiguration.entryTtl(redisProperties.getTimeToLive());
}
if (StringUtils.hasText(redisProperties.getKeyPrefix())) {
redisCacheConfiguration = redisCacheConfiguration.prefixCacheNameWith(redisProperties.getKeyPrefix());
}
if (!redisProperties.isCacheNullValues()) {
redisCacheConfiguration = redisCacheConfiguration.disableCachingNullValues();
}
if (!redisProperties.isUseKeyPrefix()) {
redisCacheConfiguration = redisCacheConfiguration.disableKeyPrefix();
}
// 缓存对象中可能会有 LocalTime/LocalDate/LocalDateTime 等 java.time 段,所以需要通过 JavaTimeModule 定义其序列化、反序列化格式
JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern("HH:mm:ss.SSSSSSSSS")));
javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSSSSSSSS")));
javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern("HH:mm:ss.SSSSSSSSS")));
javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSSSSSSSS")));
// 基于 Jackson 的 RedisSerializer 实现:GenericJackson2JsonRedisSerializer
GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer();
// 把 javaTimeModule 配置到 Serializer 中
serializer = serializer.configure(config -> {
config.registerModules(javaTimeModule);
});
// 设置 Value 的序列化方式
return redisCacheConfiguration
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer));
}
}
- 使用 使用就比较简单了,声明@EnableCaching,@EnableCaching为开启缓存,可以放在任何一个能被自动加载的地方。
比如放在启动类上,或者放在你的redis配置上,比如我放在redis的序列化配置上
代码语言:javascript复制package com.litblc.fmock.moduleA.config;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* 使redis保存的数据支持<string,object>,并保存的数据为json字符串
*
* @Author zhenhuaixiu
* @Date 2023/11/6 10:51
* @Version 1.0
*/
@Configuration
@EnableCaching // @EnableCaching为开启缓存,可以放在任何一个能被自动加载的地方
public class RedisTemplateConfig extends RedisTemplate<String, Object> {
public RedisTemplateConfig(RedisConnectionFactory redisConnectionFactory) {
// 构造函数注入 RedisConnectionFactory,设置到父类
super.setConnectionFactory(redisConnectionFactory);
// 使用 Jackson 提供的通用 Serializer
GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer();
serializer.configure(mapper -> {
// 如果涉及到对 java.time 类型的序列化,反序列化那么需要注册 JavaTimeModule
mapper.registerModule(new JavaTimeModule());
});
// String 类型的 key/value 序列化
super.setKeySerializer(StringRedisSerializer.UTF_8);
super.setValueSerializer(serializer);
// Hash 类型的 key/value 序列化
super.setHashKeySerializer(StringRedisSerializer.UTF_8);
super.setHashValueSerializer(serializer);
}
}
- 控制器只需要声明
@Cacheable
即可
@GetMapping(value = "listDesc")
@Operation(summary = "获取文章列表")
@Cacheable(value = "posts")
public List<Posts> postsListDesc() {
System.out.println("再次访问这个接口,这句话不会输出,证明走了缓存");
List<Posts> res = this.postsService.getAllPosts();
this.redisTemplate.opsForValue().set("posts1", res, 60L, TimeUnit.SECONDS); // 手动存储的是字符串
this.redisTemplate.opsForValue().set("posts2", res); // 永久期限,正常json格式
return res;
}
定时任务
- 需要依赖
spring-boot-starter
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
- 在启动类、或者其他能扫描到的类添加注解
@EnableScheduling
- 方法中添加
@Scheduled()
即可,如下示例代码
package com.litblc.fmock.moduleA.crontab;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* @Author zhenhuaixiu
* @Date 2023/11/10 17:03
* @Version 1.0
*/
// @Scheduled 参数可以接受两种定时的设置,一种是我们常用的`cron="*/6 * * * * ?"`,一种是 fixedRate = 6000,两种都表示每隔六秒跑一次。
// @Scheduled(fixedRate = 6000) :上一次开始执行时间点之后6秒再执行
// @Scheduled(fixedDelay = 6000) :上一次执行完毕时间点之后6秒再执行
// @Scheduled(initialDelay=1000, fixedRate=6000) :第一次延迟1秒后执行,之后按 fixedRate 的规则每6秒执行一次
// cron參考 https://blog.csdn.net/Linweiqiang5/article/details/86741258
@EnableScheduling
@Component
public class SchedulerTask {
private int count = 0;
@Scheduled(cron = "*/6 * * * * ?")
private void task1() {
System.out.println("这样就执行定时任务,第几次:" ( this.count));
}
@Scheduled(fixedRate = 6000)
public void task2() {
System.out.println("现在时间:" (new Date()));
}
}
在单应用中可以这么使用,比较简单,但是在微服务架构中就不行了,比如启用了多个实例,那么定时任务也会执行多次。 所以后面我们一点一点完善升级。
#QR{padding-top:20px;} #QR a{border:0} #QR img{width:180px;max-width:100%;display:inline-block;margin:.8em 2em 0 2em} #rewardButton { border: 1px solid #ccc; /*width: 20%;*/ line-height: 53px; text-align: center; height: 70px; display: block; border-radius: 10px; -webkit-transition-duration: .4s; transition-duration: .4s; background-color: #f77b83; color: #f7f7f7; margin: 20px auto; padding: 8px 25px; } #rewardButton:hover { color: #eb5055; border-color: #f77b83; outline-style: none; background-color: floralwhite; }
本文由 litblc 创作,采用 知识共享署名4.0 国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
最后编辑时间为: Nov 10, 2023 at 05:40 pm