1.注册中心
选型eureka
双节点。AP模型,因为目前已有的项目使用的是eureka apollo方案。nacos集成配置中心 注册中心双双功能确实很强大,为了快速开发,再加上团队对springcloud整套框架的熟悉度,本次优先使用eureka作为注册中心,简单易用。基于k8s部署。
1.1 maven依赖
代码语言:javascript复制<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
1.2 关键配置
代码语言:javascript复制#关闭自我保护机制,Eureka Server在运行期间,会统计心跳失败的比例在15分钟之内是否低于85%,如果出现低于的情况(在单机调试的时候很容易满足,实际在生产环境上通常是由于网络不稳定导致),Eureka Server会将当前的实例注册信息保护起来,同时提示这个警告。保护模式主要用于一组客户端和Eureka Server之间存在网络分区场景下的保护。
#Eureka Server将会尝试保护其服务注册表中的信息,不再删除服务注册表中的数据(也就是不会注销任何微服务)。
eureka.server.enable-self-preservation=false
#指示eureka 服务器从收到最后一次心跳后等待的时间(以秒为单位),默认值90,然后它可以从其视图中删除此实例,并禁止流量到此实例。
eureka.instance.lease-expiration-duration-in-seconds=300
# 以IP地址注册到服务中心,相互注册使用IP地址
eureka.instance.preferIpAddress
#读取对等服务器节点复制数据的超时时间 默认值200
eureka.server.peer-node-read-timeout-ms=60000
# 清理无效节点的时间间隔,默认60秒
eureka.server.eviction-interval-timer-in-ms=300000
# 在Spring Cloud中,服务的Instance ID的默认值是${spring.cloud.client.hostname}:${spring.application.name}:${spring.application.instance_id:${server.port}} ,也就是机器主机名:应用名称:应用端口 。因此在Eureka Server首页中看到的服务的信息类似如下:itmuch:microservice-provider-user:8000
eureka.instance.instance-id: ${spring.cloud.client.ipAddress}:${server.port} # 将Instance ID设置成IP:端口的形式
代码语言:javascript复制#eureka client间隔多久去拉取服务注册信息,默认为30秒,对于api-gateway,如果要迅速获取服务注册状态,可以缩小该值,比如5秒
eureka.client.registry-fetch-interval-seconds=30
#指示 eureka 客户端需要多久(以秒为单位)向 eureka 服务器发送心跳以指示它仍然存在。如果在 leaseExpirationDurationInSeconds 指定的时间内没有收到心跳,eureka 服务器将从它的视图中删除该实例,从而禁止流量到该实例。,如果该instance实现了HealthCheckCallback,并决定让自己unavailable的话,则该instance也不会接收到流量。
eureka.instance.lease-renewal-interval-in-seconds=30
2.声明式Http服务调用
选型OpenFeign
。现在Springcloud alibaba
这一套很流行,我们也曾经尝试使用dubbo作为rpc进行各个微服务之间的调用。很丝滑,不过我们暂时需要这种基于socket连接的RPC,性能强劲,使用声明式Http服务,基于Springmvc注解进行开发,团队成员也很熟练,开箱即用,所以本次选用OpenFeign
。
2.1 maven依赖
代码语言:javascript复制<!-- classpath中存在client依赖,会自动注册 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
2.2 启用Feign
代码语言:javascript复制@SpringBootApplication
@RestController
@EnableFeignClients
public class Application {
@RequestMapping("/")
public String home() {
return "Hello world";
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
2.2.1 contextId属性的作用
在@FeignClient
注解中有个属性contextId
,平时我们项目中只使用了name
属性或者value
属性,他们作用都是一样的,标识服务提供者的名称。
如果我们使用Feign定义了两个接口,但是目标服务是同一个的时候:
代码语言:javascript复制@FeignClient(value = "ORDER-SERVICE")
@RequestMapping("/order")
public interface OrderApi2 {
@GetMapping("/orderDetail/{id}")
String getOrderDetail(@PathVariable("id") int id);
}
@FeignClient(value = "ORDER-SERVICE")
@RequestMapping("/order")
public interface OrderApi {
@GetMapping("/get")
String getOrder(@RequestParam("id") int id);
}
此时项目会报错:
代码语言:javascript复制***************************
APPLICATION FAILED TO START
***************************
Description:
The bean 'ORDER-SERVICE.FeignClientSpecification' could not be registered. A bean with that name has already been defined and overriding is disabled.
Action:
Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true
产生的原因是:
代码语言:javascript复制
Map<String, Object> attributes = annotationMetadata
.getAnnotationAttributes(FeignClient.class.getCanonicalName());
String name = getClientName(attributes);
registerClientConfiguration(registry, name,
attributes.get("configuration"));
// getClientName具体实现
private String getClientName(Map<String, Object> client) {
if (client == null) {
return null;
}
String value = (String) client.get("contextId");
if (!StringUtils.hasText(value)) {
value = (String) client.get("value");
}
//省略部分代码
}
已上代码是对每一个FeignClient注册流程的一段代码,其中,对于每一个FeignClient都会创建一个{clientName}.FeignClientSpecification
。就是这里因为contextId
取值取不到时会获取value的值,所以这里就会重复报错。。
解决方案有2种:
- 根据错误提示的解决方案,设置配置属性
spring.main.allow-bean-definition-overriding=true
,名称相同的bean是否支持覆盖。这个属性值在spring中默认是true,在springboot中默认是false。 - 我们就要用到contextId了。contextId设置之后,就是clientName的名字,一旦这里区分了之后,就能解决上面的错误。 @FeignClient(value = "ORDER-SERVICE",contextId = "order1") @RequestMapping("/order") public interface OrderApi2 { } @FeignClient(value = "ORDER-SERVICE",contextId = "order2") @RequestMapping("/order") public interface OrderApi { }
2.2.2 Ambiguous mapping的解决
微服务结构下,项目中为了方便其他服务的调用,会将FeignClient
接口都放到一个api jar
包,方便其他项目引入。此时一般都是如下定义FeignClient
:
@FeignClient(value = "ORDER-SERVICE")
@RequestMapping("/order")
public interface OrderApi {
@GetMapping("/get")
String getOrder(@RequestParam("id") int id);
}
如果为了方便定义,在服务的controller层实现了已上接口:
代码语言:javascript复制@RestController
public class OrderRest implements OrderApi {
@Override
public String getOrder(int id) {
return "hello world";
}
}
那么,此时会报错:
代码语言:javascript复制Caused by: java.lang.IllegalStateException: Ambiguous mapping. Cannot map 'cn.ev.order.api.OrderApi' method
cn.ev.order.api.OrderApi#getOrder(int)
遇到这种错误,开发人员首先肯定是排查方法有没有重复映射,有没有重复的Mapping或者在实现的时候是否重复添加了mapping注解。但是在这里都不是。产生这种问题的根本原因是RequestMappingHandlerMapping
在处理映射时,判断的条件是:存在Controller
注解或RequestMapping
注解,就会被判定位isHandler
,而我们的FeignCLient接口中有@RequestMapping,也会进行映射注册,此时就会产生冲突。
// path:"org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java"
@Override
protected boolean isHandler(Class<?> beanType) {
return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
}
解决方案有2种:
- 简单解决,开发者只需要注释掉
//@RequestMapping("/order")
,将映射的公共部分/order
放置到每一个方法上 @FeignClient(value = "ORDER-SERVICE") //@RequestMapping("/order") public interface OrderApi { @GetMapping("/order/get") String getOrder(@RequestParam("id") int id); } - 深入解决,解决FeignClient接口的Mapping映射问题,重写MappingHandler映射方法 @Configuration @ConditionalOnClass({Feign.class}) public class FeignMappingDefaultConfiguration { @Bean public WebMvcRegistrations feignWebRegistrations() { return new WebMvcRegistrationsAdapter() { @Override public RequestMappingHandlerMapping getRequestMappingHandlerMapping() { return new FeignFilterRequestMappingHandlerMapping(); } }; } private static class FeignFilterRequestMappingHandlerMapping extends RequestMappingHandlerMapping { @Override protected boolean isHandler(Class<?> beanType) { return super.isHandler(beanType) && (AnnotationUtils.findAnnotation(beanType, RestController.class) != null); } } }
2.2.3 Feign Encoder&Decoder
如果没有自定义Decoder
,那么对于Feign返回会使用AsyncResponseHandler
异步响应处理器进行处理。其中使用的默认Decoder
是SpringDecoder
,并且用OptionalDecoder
包装了一下。
ErrorDecoder
处理器处理,非200 <=状态码 < 300
或者非 【如果是404 并且配置了decode404
= true 并且返回值不是void】,其他错误都会走错误处理器。
2.2.4 自定义异常处理
项目中有2处的自定义已异常需要处理:
- 对常见的controller层抛出的业务异常需要做全局异常处理
- 基于Feign调用的服务端报错,客户端对于异常的处理。
问题1的处理:
老生常谈,使用@RestControllerAdvice
做全局异常拦截,对于自定义异常可以进行自定义处理。
问题2的处理:
需要借助ErrorDecoder
Feign的错误处理器处理。
3.ORM框架
选用MybatisPlus
做ORM映射。可以方便地使用框架自带丰富的扩展功能。
3.1 自定义主键策略
自定义主键生成器,并且可以通过对一些注解的处理进行表级别的id个性化设置。以下主要是通过@TableName
获取了表名,并且根据表名去路由获取主键初始值和自增步长从而计算nextId
。我们可以从数据库获取,也可以从redis中获取。
@Component
public class CustomIdGenerator implements IdentifierGenerator {
@Autowired
private DBSequenceIdGenerator generator;
public void setGenerator(DBSequenceIdGenerator generator) {
this.generator = generator;
}
@Override
public Number nextId(Object entity) {
TableName tableName = entity.getClass().getAnnotation(TableName.class);
return new Long(generator.genBusinessId(tableName.value(),8));
}
}
3.2 多数据源支持
MybatisPlus
对多数据源的支持属于配置配置开箱即用了。
引入多数据依赖
代码语言:javascript复制<!--MybatisPlus 多数据源-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>2.5.6</version>
</dependency>
在application.properties
中配置多个数据源
# ds1
spring.datasource.dynamic.datasource.ds1.url=
spring.datasource.dynamic.datasource.ds1.username=
spring.datasource.dynamic.datasource.ds1.password=
spring.datasource.dynamic.datasource.ds1.driver-class-name=com.mysql.cj.jdbc.Driver
# ds2
spring.datasource.dynamic.datasource.ds2.url=
spring.datasource.dynamic.datasource.ds2.username=
spring.datasource.dynamic.datasource.ds2.password=
spring.datasource.dynamic.datasource.ds2.driver-class-name=com.mysql.cj.jdbc.Driver
使用@DS
进行多数据源的切换,既可以在方法上使用,也可以在类上面使用。
@DS("ds2")
public interface DemoMapper extends BaseMapper<Demo> {}
@Service
@DS("slave")
public class DemoServiceImpl implements DemoService {
@Override
@DS("ds1")
public List selec() {
}
}
4.配置中心
Apollo配置中心,运维上有现成的支持,简单易用,团队成员也比较熟悉。
4.1 引入maven依赖
代码语言:javascript复制<dependency>
<groupId>com.ctrip.framework.apollo</groupId>
<artifactId>apollo-client</artifactId>
<version>1.8.0</version>
</dependency>
4.2 客户端接入
代码语言:javascript复制@EnableApolloConfig(value = {"namespace1","namespace2"})
public class Application {
}
通过EnableApolloConfig
注解启用apollo的配置能力,配置需要监听的多个namespace下的配置信息,
4.3 刷新配置
apollo的客户端配置的更新是通过定时轮询 长轮询,相互补充从而达到“实时”刷新的效果。如果需要监听某个特定的key发生变化,用如下方式@ApolloConfigChangeListener
均可以实现。
@ApolloConfigChangeListener
private void userConfigChangeHandler(ConfigChangeEvent changeEvent) {
if (changeEvent.isChanged(KEY_USER_CACHE_REFRESH)) {
init();
}
}
@ApolloConfigChangeListener(interestedKeys = {KEY_USER_CACHE_REFRESH})
private void userConfigChangeHandler1(ConfigChangeEvent changeEvent) {
init1();
}