# 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;
    }
}
Last Updated: 12/15/2023, 8:18:50 AM