# 01-SringCloud 各组件集成与配置
# 一、RestTemplate配置
# 1、自动添加token
/**
* @Author qixiaodong
* @Date 2020/11/2 12:28
*/
@Component
public class RestTemplateTokenInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
HttpHeaders headers = request.getHeaders();
headers.add(TokenEnum.ALI_TOKEN_KEY, TokenThreadLocal.getAliToken());
String userToken = TokenThreadLocal.getUserToken();
if (ToolUtil.isNotEmpty(userToken)) {
headers.add(TokenEnum.TOKEN_KEY, userToken);
String cookie = TokenEnum.TOKEN_KEY + "=" + userToken;
headers.add("Cookie", cookie);
}
return execution.execute(request, body);
}
}
# 2、OkHttp对RestTemplate的支持
/**
* Feign 配置okhttp
*
* @Author qixiaodong
* @Date 2020/8/28 16:58
*/
@Configuration
public class RestTemplateConfig {
@Autowired
private RestTemplateTokenInterceptor restTemplateTokenInterceptor;
@Autowired
private FeignClientProperties feignClientProperties;
/**
* 基于OkHttp3配置RestTemplate
*/
@Bean
public RestTemplate restTemplate(OkHttpClient okHttpClient) {
OkHttp3ClientHttpRequestFactory okHttp3ClientHttpRequestFactory =
new OkHttp3ClientHttpRequestFactory(okHttpClient);
FeignClientProperties.FeignClientConfiguration feignClientConfiguration =
feignClientProperties.getConfig().get(feignClientProperties.getDefaultConfig());
okHttp3ClientHttpRequestFactory.setConnectTimeout(feignClientConfiguration.getConnectTimeout());
okHttp3ClientHttpRequestFactory.setReadTimeout(feignClientConfiguration.getReadTimeout());
okHttp3ClientHttpRequestFactory.setWriteTimeout(feignClientConfiguration.getReadTimeout());
RestTemplate restTemplate = new RestTemplate(okHttp3ClientHttpRequestFactory);
restTemplate.setInterceptors(Collections.singletonList(restTemplateTokenInterceptor));
return restTemplate;
}
}
# 二、OpenFeign集成与配置
Feign和OpenFeign的区别?
- Feign是Spring cloud组件中的一个轻量级Restful的HTTP服务客户端,Feign内置了Ribbon,用来做客户端负载均衡,去调用服务注册中心的服务。Feign的使用方式是:使用Feign的注解定义接口,调用这个接口,就可以调用服务注册中心的服务
- OpenFeign是spring cloud在Feign的基础上支持了SpringMVC的注解,如@RequestMapping等等。OpenFeign的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务
# 1、引入依赖
<!-- openfeign 依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- okhttp对feign的支持 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
# 2、开启使用
添加 @EnableFeignClients 注解
# 3、配置
feign:
client:
config:
default:
connectTimeout: 2000
#feign超时时间设置
readTimeout: 5000
#feign日志
loggerLevel: FULL
#关闭httpclient
httpclient:
enabled: false
#开启使用okhttp
okhttp:
enabled: true
# 4、优化配置-使用okhttp
在默认情况下,Client的实现类是Client.Default。Client.Default是由HttpURLConnection来实现网络请求的。缺乏连接池的支持。
首先查看FeignRibbonClient的自动配置类FeignRibbonClientAutoConfiguration,该类在程序启动的时候注入一些Bean,其中注入了一个BeanName为feignClient的Client类型的Bean。在省缺配置BeanName为FeignClient的Bean的情况下,会自动注入Client.Default这个对象,跟踪Client.Default源码,Client.Default使用的网络请求框架是HttpURLConnection,代码如下
public static class Default implements Client {
private final SSLSocketFactory sslContextFactory;
private final HostnameVerifier hostnameVerifier;
public Default(SSLSocketFactory sslContextFactory, HostnameVerifier hostnameVerifier) {
this.sslContextFactory = sslContextFactory;
this.hostnameVerifier = hostnameVerifier;
}
public Response execute(Request request, Options options) throws IOException {
HttpURLConnection connection = this.convertAndSend(request, options);
return this.convertResponse(connection, request);
}
......//代码省略
}
# (1) 如何使用HttpClient
查看FeignAutoConfiguration.HttpClientFeignConfiguration的源码:
从代码@ConditionalOnClass({ApacheHttpClient.class})注解可知,只需要在pom文件上加上HttpClient依赖即可。另外需要在配置文件中配置feign.httpclient.enabled为true,从@ConditionalOnProperty注解可知,这个配置可以不写,因为在默认情况下就为true。
@Configuration
@ConditionalOnClass({ApacheHttpClient.class})
@ConditionalOnMissingClass({"com.netflix.loadbalancer.ILoadBalancer"})
@ConditionalOnMissingBean({CloseableHttpClient.class})
@ConditionalOnProperty(
value = {"feign.httpclient.enabled"},
matchIfMissing = true
)
protected static class HttpClientFeignConfiguration {
private final Timer connectionManagerTimer = new Timer("FeignApacheHttpClientConfiguration.connectionManagerTimer", true);
@Autowired(
required = false
)
private RegistryBuilder registryBuilder;
private CloseableHttpClient httpClient;
protected HttpClientFeignConfiguration() {
}
@Bean
@ConditionalOnMissingBean({HttpClientConnectionManager.class})
public HttpClientConnectionManager connectionManager(
ApacheHttpClientConnectionManagerFactory connectionManagerFactory, FeignHttpClientProperties httpClientProperties) {
final HttpClientConnectionManager connectionManager = connectionManagerFactory.newConnectionManager(
httpClientProperties.isDisableSslValidation(),
httpClientProperties.getMaxConnections(),
httpClientProperties.getMaxConnectionsPerRoute(), httpClientProperties.getTimeToLive(),
httpClientProperties.getTimeToLiveUnit(), this.registryBuilder);
this.connectionManagerTimer.schedule(new TimerTask() {
public void run() {
connectionManager.closeExpiredConnections();
}
}, 30000L, (long) httpClientProperties.getConnectionTimerRepeat());
return connectionManager;
}
@Bean
public CloseableHttpClient httpClient(
ApacheHttpClientFactory httpClientFactory,
HttpClientConnectionManager httpClientConnectionManager, FeignHttpClientProperties httpClientProperties) {
RequestConfig defaultRequestConfig = RequestConfig.custom()
.setConnectTimeout(httpClientProperties.getConnectionTimeout()).setRedirectsEnabled(httpClientProperties.isFollowRedirects()).build();
this.httpClient =
httpClientFactory.createBuilder().setConnectionManager(httpClientConnectionManager).setDefaultRequestConfig(defaultRequestConfig).build();
return this.httpClient;
}
@Bean
@ConditionalOnMissingBean({Client.class})
public Client feignClient(HttpClient httpClient) {
return new ApacheHttpClient(httpClient);
}
@PreDestroy
public void destroy() throws Exception {
this.connectionManagerTimer.cancel();
if (this.httpClient != null) {
this.httpClient.close();
}
}
}
# (2) 如何使用OkHttp
查看FeignAutoConfiguration.HttpClientFeignConfiguration的源码: 如果想要在Feign中使用OkHttp作为网络请求框架,则只需要在pom文件中加上feign-okhttp的依赖 源码如下:
@Configuration
@ConditionalOnClass({OkHttpClient.class})
@ConditionalOnMissingClass({"com.netflix.loadbalancer.ILoadBalancer"})
@ConditionalOnMissingBean({okhttp3.OkHttpClient.class})
@ConditionalOnProperty({"feign.okhttp.enabled"})
protected static class OkHttpFeignConfiguration {
private okhttp3.OkHttpClient okHttpClient;
protected OkHttpFeignConfiguration() {
}
@Bean
@ConditionalOnMissingBean({ConnectionPool.class})
public ConnectionPool httpClientConnectionPool(FeignHttpClientProperties httpClientProperties, OkHttpClientConnectionPoolFactory connectionPoolFactory) {
Integer maxTotalConnections = httpClientProperties.getMaxConnections();
Long timeToLive = httpClientProperties.getTimeToLive();
TimeUnit ttlUnit = httpClientProperties.getTimeToLiveUnit();
return connectionPoolFactory.create(maxTotalConnections, timeToLive, ttlUnit);
}
@Bean
public okhttp3.OkHttpClient client(OkHttpClientFactory httpClientFactory, ConnectionPool connectionPool, FeignHttpClientProperties httpClientProperties) {
Boolean followRedirects = httpClientProperties.isFollowRedirects();
Integer connectTimeout = httpClientProperties.getConnectionTimeout();
Boolean disableSslValidation = httpClientProperties.isDisableSslValidation();
this.okHttpClient = httpClientFactory.createBuilder(disableSslValidation).connectTimeout((long)connectTimeout, TimeUnit.MILLISECONDS).followRedirects(followRedirects).connectionPool(connectionPool).build();
return this.okHttpClient;
}
@PreDestroy
public void destroy() {
if (this.okHttpClient != null) {
this.okHttpClient.dispatcher().executorService().shutdown();
this.okHttpClient.connectionPool().evictAll();
}
}
@Bean
@ConditionalOnMissingBean({Client.class})
public Client feignClient(okhttp3.OkHttpClient client) {
return new OkHttpClient(client);
}
}
实际使用中发现,只引入依赖实际并没有使用okhttp,需要以下配置(待验证):
/**
* Feign 配置okhttp
*
* @Author qixiaodong
* @Date 2020/8/28 16:58
*/
@Configuration
@ConditionalOnClass(Feign.class)
@AutoConfigureBefore(FeignAutoConfiguration.class)
public class FeignConfig {
@Bean
@ConditionalOnMissingBean({ConnectionPool.class})
public ConnectionPool httpClientConnectionPool(FeignHttpClientProperties httpClientProperties, OkHttpClientConnectionPoolFactory connectionPoolFactory) {
Integer maxTotalConnections = httpClientProperties.getMaxConnections();
Long timeToLive = httpClientProperties.getTimeToLive();
TimeUnit ttlUnit = httpClientProperties.getTimeToLiveUnit();
return connectionPoolFactory.create(maxTotalConnections, timeToLive, ttlUnit);
}
@Bean
public OkHttpClient client(OkHttpClientFactory httpClientFactory, ConnectionPool connectionPool, FeignHttpClientProperties httpClientProperties) {
Boolean followRedirects = httpClientProperties.isFollowRedirects();
Integer connectTimeout = httpClientProperties.getConnectionTimeout();
Boolean disableSslValidation = httpClientProperties.isDisableSslValidation();
return httpClientFactory.createBuilder(disableSslValidation)
.connectTimeout((long) connectTimeout, TimeUnit.MILLISECONDS)
.followRedirects(followRedirects)
.connectionPool(connectionPool)
.retryOnConnectionFailure(true)
.build();
}
}
# 5、优化配置
# (1)Feign日志
Feign调用日志,默认是Debug级别,参照默认实现 Slf4jLogger ,实现自定义日志
/**
* @Author qixiaodong
* @Date 2020/11/12 12:03
*/
public class FeignLogger extends feign.Logger {
private final org.slf4j.Logger logger;
public FeignLogger() {
this(feign.Logger.class);
}
public FeignLogger(Class<?> clazz) {
this(LoggerFactory.getLogger(clazz));
}
public FeignLogger(String name) {
this(LoggerFactory.getLogger(name));
}
FeignLogger(org.slf4j.Logger logger) {
this.logger = logger;
}
@Override
protected void logRequest(String configKey, Level logLevel, Request request) {
//增加info级别日志输出
if (logger.isDebugEnabled() || logger.isInfoEnabled()) {
super.logRequest(configKey, logLevel, request);
}
}
@Override
protected Response logAndRebufferResponse(String configKey, Level logLevel,
Response response, long elapsedTime) throws IOException {
//增加info级别日志输出
if (logger.isDebugEnabled() || logger.isInfoEnabled()) {
return super.logAndRebufferResponse(configKey, logLevel, response, elapsedTime);
}
return response;
}
@Override
protected void log(String configKey, String format, Object... args) {
// Not using SLF4J's support for parameterized messages
// (even though it would be more efficient) because it would
// require the incoming message formats to be SLF4J-specific.
if (logger.isDebugEnabled()) {
logger.debug(String.format(methodTag(configKey) + format, args));
} else if (logger.isInfoEnabled()) {
//增加info级别日志输出
logger.info(String.format(methodTag(configKey) + format, args));
}
}
}
配置使用自定义日志
@Bean
Logger feignLogger() {
return new FeignLogger();
}
# (2)自动添加token
/**
* @Author qixiaodong
* @Date 2020/9/7 9:26
*/
@Slf4j
@Component
public class FeignRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
/**
* hystrix 隔离策略会导致 RequestContextHolder.getRequestAttributes()返回null
* 多线程下,RequestContextHolder.getRequestAttributes()也会返回null
*/
//添加token
String userToken = TokenThreadLocal.getUserToken();
if (ToolUtil.isNotEmpty(userToken)) {
requestTemplate.header(TokenEnum.TOKEN_KEY, userToken);
String cookie = TokenEnum.TOKEN_KEY + "=" + userToken;
Map<String, Collection<String>> headers = requestTemplate.headers();
if (headers.containsKey("Cookie")) {
headers.get("Cookie").add(cookie);
} else {
requestTemplate.header("Cookie", cookie);
}
}
}
}
# 三、Loadbalancer集成与配置
# 1、引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
# 2、服务配置
spring:
cloud:
loadbalancer:
retry:
enabled: false
#关闭ribbon 自动启用 loadbalancer
ribbon:
enabled: false
# 四、Sentinel集成与配置
# 1、引入依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
# 2、开启服务熔断
添加 @EnableCircuitBreaker 注解, @SpringCloudApplication 注解已包含。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
public @interface SpringCloudApplication {
}
# 3、服务配置
spring:
cloud:
sentinel:
#设置是否开启 Sentinel,默认为 true 开启,所以一般不用主动设置。如果关闭 Sentinel 的功能,
#例如说在本地开发的时候,可以设置为 false 关闭。项目目前未启动sentinel控制台
enabled: false
#设置是否饥饿加载,默认为 false 关闭。默认情况下,Sentinel 是延迟初始化,在首次使用到 #Sentinel 才进行初始化。
#通过设置为 true 时,在项目启动时就会将 Sentinel 直接初始化,完成向 Sentinel 控制台进行注册
eager: true
#Sentinel 控制台地址
transport:
dashboard: 127.0.0.1:8080
#设置拦截请求的地址,默认为 /* (只能拦截根目录的请求)
filter:
url-patterns: /**
# 4、开启sentinel对feign的支持
feign:
httpclient:
enabled: false
okhttp:
enabled: true
#开启sentinel对feign的支持
sentinel:
enabled: true
# 5、降级异常处理
sentinel异常拦截器BlockExceptionHandler,默认提供:DefaultBlockExceptionHandler 只输出字符串"Blocked by Sentinel (flow limiting)"
需要实现自定义异常拦截处理,可以由全局异常处理器处理
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 请求被sentinel拦截后,会抛出 BlockException
* AuthorityException 黑白名单控制
* DegradeException 熔断降级
* FlowException 流量控制
* ParamFlowException 热点参数限流
* SystemBlockException 系统自适应限流
*
* @Author qixiaodong
* @Date 2020/12/4 10:04
*/
@Component
public class CustomBlockExceptionHandler implements BlockExceptionHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws Exception {
throw e;
}
}
# 五、注册中心
注解开启:@EnableDiscoveryClient
# 1、Nacos
# (1)引入依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
# (2)注解开启
添加:@EnableDiscoveryClient 注解, @SpringCloudApplication 注解已包含。
# (3)配置使用:
spring:
cloud:
nacos:
discovery:
server-addr: url:8848
# test服务器启动的是docker容器,ip地址和宿主机ip不同。通过服务名调用时,会报错:链接超时
#ip: 10.10.210.252
# 2、Eureka
# 六、配置中心
# 1、Nacos
# 2、Apollo(携程:阿波罗)
# (1)引入依赖:
<dependency>
<groupId>com.ctrip.framework.apollo</groupId>
<artifactId>apollo-client</artifactId>
<version>1.6.0</version>
</dependency>
# (2)开启使用
添加注解:@EnableApolloConfig
# (3)配置:
app:
id: business.center
apollo:
bootstrap:
enabled: true
eagerLoad:
enabled: true
refreshInterval: 100
meta: http://IP:8080
#缓存目录
cacheDir: ../cache/
# (4)配置自动刷新:
import com.ctrip.framework.apollo.model.ConfigChangeEvent;
import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener;
import com.ctrip.framework.apollo.spring.annotation.EnableApolloConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.cloud.context.environment.EnvironmentChangeEvent;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Configuration;
/**
* @Author qixiaodong
* @Date 2020/11/4 16:33
*/
@Slf4j
@EnableApolloConfig
@Configuration
public class ApolloConfig implements ApplicationContextAware {
private ApplicationContext applicationContext;
@ApolloConfigChangeListener(value = {"application"})
private void onChangeToAll(ConfigChangeEvent changeEvent) {
for (String changedKey : changeEvent.changedKeys()) {
log.info("Apollo 更新:{} --> {}", changedKey, changeEvent.getChange(changedKey).getNewValue());
}
this.applicationContext.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys()));
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}