九、微服务进阶之 ElasticSearch(基础配置)

九、微服务进阶之 ElasticSearch(基础配置)

功能介绍

抽离出 elasticsearch 的功能模块,单独封装配置信息,保证每个业务模块引入后使用的是相同的一套配置,避免每个需要使用 es 的模块单独配置,减少冗余的代码

代码展示

自定义 es 的配置属性,即一些连接地址信息; ElasticValueConfiguration

@ConfigurationProperties(prefix = "spring.elastic")
@Data
public class ElasticValueConfiguration {
    /** * es节点 */
    private List uris;
    /** * 用户名 */
    private String name;
    /** * 密码 */
    private String password;
    /** * 是否开启es连接 */
    private boolean connectEnabled;
    /** * 连接超时时间 */
    private int connectTimeout;
    /** * 会话超时时间 */
    private int socketTimeout;
    /** * 获取连接超时时间 */
    private int requestTimeout;
    /** * 最大连接数 */
    private int maxConnect;
    /** * 最大路由数连接数 */
    private int maxConnectRoute;
}

ElasticValueConfiguration 属性配置类对应的配置名如下

"spring.elastic.connect-enabled":"是否开启es连接" 
"spring.elastic.connect-timeout":"连接超时时间" 
"spring.elastic.max-connect": "最大连接数" 
"spring.elastic.max-connect-route": "最大路由数连接数" 
"spring.elastic.name": "用户名" 
"spring.elastic.password": "密码" 
"spring.elastic.request-timeout": "获取连接超时时间" 
"spring.elastic.socket-timeout": "会话超时时间" 
"spring.elastic.uris": "es节点,资源路径"

ElasticSearchConfiguration 自定义连接配置

@SpringBootConfiguration
@EnableConfigurationProperties(ElasticValueConfiguration.class)
@Slf4j
public class ElasticSearchConfiguration {
    @Autowired
    private ElasticValueConfiguration elasticValueConfiguration;

    /** * 同名覆盖默认的 RestHighLevelClient */
    @Bean
    RestHighLevelClient restHighLevelClient() {
        log.info("elastic connection begin to set up...");
        if (!elasticValueConfiguration.isConnectEnabled()) {
            return null;
        }
        List httpHosts = new ArrayList<>();
        for (String es : elasticValueConfiguration.getUris()) {
            httpHosts.add(new HttpHost(es));
        }
        final CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
        // 需要用户名和密码的认证 
        credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(elasticValueConfiguration.getName(), elasticValueConfiguration.getPassword()));
        RestClientBuilder restClientBuilder = RestClient.builder(httpHosts.toArray(new HttpHost[0]));
        // 异步连接httpclient配置 
        restClientBuilder.setHttpClientConfigCallback(httpAsyncClientBuilder -> {
            httpAsyncClientBuilder.setDefaultCredentialsProvider(credentialsProvider);
            httpAsyncClientBuilder.setMaxConnTotal(elasticValueConfiguration.getMaxConnect());
            httpAsyncClientBuilder.setMaxConnPerRoute(elasticValueConfiguration.getMaxConnectRoute());
            return httpAsyncClientBuilder;
        });
        // 异步连接httpclient延时配置 
        restClientBuilder.setRequestConfigCallback(requestConfigBuilder -> {
            requestConfigBuilder.setConnectTimeout(elasticValueConfiguration.getConnectTimeout());
            requestConfigBuilder.setSocketTimeout(elasticValueConfiguration.getSocketTimeout());
            requestConfigBuilder.setConnectionRequestTimeout(elasticValueConfiguration.getRequestTimeout());
            return requestConfigBuilder;
        });
        // 设置一个监听程序,每次节点发生故障时都会收到通知,这样就可以采取相应的措施。 
        restClientBuilder.setFailureListener(new RestClient.FailureListener() {
            public void onFailure(HttpHost host) {
                log.warn("elastic nodes {} error!!!", host.toHostString());
            }
        });
        log.info("elastic connection set successfully!");
        return new RestHighLevelClient(restClientBuilder);
    }
}
常用依赖

特别说明:
当前 springboot 版本为 2.2.1.RELEASE; 此版本下自带了 es 的版本变量, 版本号为 6.8.4; 由于自己服务器上面使用的 es 版本为 7.11.2, 所以 es 的依赖版本尽量和所使用的 es 服务版本保持一致

<properties>
    <elasticsearch.version>7.11.2</elasticsearch.version>
</properties>
<dependencies>
    <!-- 自定义配置参数提示 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
        <optional>true</optional>
        <!-- 不传递依赖,只是一个工具依赖 -->
    </dependency>
    <!-- 取消低版本es -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
        <exclusions>
            <exclusion>
                <groupId>org.elasticsearch.client</groupId>
                <artifactId>elasticsearch-rest-high-level-client</artifactId>
            </exclusion>
            <exclusion>
                <groupId>org.elasticsearch.client</groupId>
                <artifactId>transport</artifactId>
            </exclusion>
            <exclusion>
                <groupId>org.elasticsearch.plugin</groupId>
                <artifactId>transport-netty4-client</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <!-- 更换高版本的es,对应es服务端的版本 -->
    <!-- 版本查看:http://ip:9200?pretty -->
    <dependency>
        <groupId>org.elasticsearch.client</groupId>
        <artifactId>elasticsearch-rest-high-level-client</artifactId>
        <version>${elasticsearch.version}</version>
    </dependency>
    <dependency>
        <groupId>org.elasticsearch.client</groupId>
        <artifactId>transport</artifactId>
        <version>${elasticsearch.version}</version>
    </dependency>
    <dependency>
        <groupId>org.elasticsearch.plugin</groupId>
        <artifactId>transport-netty4-client</artifactId>
        <version>${elasticsearch.version}</version>
    </dependency>
</dependencies>
重要说明

当前项目是为了封装 es 的功能,保证其他业务模块引入后无需多余配置也可以正常使用封装的功能,配置能被正常装载的前提是配置类可以被注册成 bean 并注入在IOC容器中;
作为外部依赖如何让自己的配置类被主程序扫描到并注册成bean ? 这里提供如下几种方式:

  1. 主程序启动类增加@Import注解,主动引入依赖中的配置类(不推荐)
  2. 主程序启动类增加@ComponentScan注解,主动扫描配置类(不推荐)
  3. 将主程序的启动类移动至其父目录,这样自定义依赖中的配置类也会被扫描到,前提是包路径名称相同(不推荐)
    • springboot启动类运行后会扫描启动类所在目录及其子目录下所有被加上了@Component的类并将其注册成bean;@Controller、@Service等注解本质也是@Component
  4. 参考官方写法:在resources目录下添加 META-INF 文件夹并创建 spring.factories 文件,将需要注册成bean的类的全名赋值给指定变量即可,如下代码
    • 此方式注入即spring的自动装配,被装配的类无需加上@Component,此种方式还可以装配自定义环境变量

主要思路:让spring容器扫描到需要注册成bean的类即可,建议使用方案4;

# 注入自定义配置类 
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ 
  com.fatcat.elastic.config.ElasticSearchConfiguration,\ 
  com.fatcat.elastic.template.ElasticDocTemplate,\ 
  com.fatcat.elastic.template.ElasticIndexTemplate
补充说明
  1. 使用默认的es版本(6.8.4)不行吗? 为什么非要修改版本? 为什么启动会报错 “type is missing”?
    • 答:如果服务端安装的版本也是低版本(低于7.X),可以不进行更换;但如果是7.X以上版本一定需要更换,由于es7.X版本后取消了映射这一规则即不需要指定 type 的值(默认 “_doc”,官方文档 elasticsearch 删除映射 ),所以依赖的 api 也发生了变化,如果使用低版本依赖连接高版本服务端,创建索引或插入数据时会报错”type is missing”; 所以务必保证服务版本和依赖版本一致,具体 Api 的使用根据版本变化即可;除此之外报错的原因还有可能是依赖在传递过程中发生了变化,具体做法参考下一问;
  2. 按照上文创建了es的依赖并指定了版本号是7.11.2, 为什么在自己的业务模块引入后es的核心依赖版本自动变为6.8.4?
    • 答:6.8.4 并非随机产生,是根据 spring-boot-dependencies 中定义的 elasticsearch.version 变量而来,这个变量值与 spring-boot 的版本号密切相关;当两个模块同时引入了 spring-boot-parent 作为父依赖并且未覆盖版本变量时,同一个外部依赖在传递中会以最后模块的 spring-boot-parent 版本中定义的此外部依赖的版本号为准;如果不理解继续看下一问;
  3. 依赖传递出现版本不一致的条件是什么?在什么情况下会出现这种问题?统一解决办法是什么?
    • 答:
      • 依赖传递描述:A 模块引入一个依赖名为 JAR 的依赖,B 模块在依赖中引入 A,这样依赖 JAR 会被传递至 B,即 B 模块也可以使用依赖 JAR 的功能;
      • 依赖版本不一致:A 模块明确指定了 JAR 的版本号为 2.0,但是 B 模块中 JAR 的版本变为 1.0;
      • 不一致前提条件:A、B 模块同时使用了 spring-boot-starter-parent 作为版本的父依赖,假设 A、B 模块中父依赖的版本是相同的;
      • 不一致的原因:查看 spring-boot-starter-parent 的父依赖 spring-boot-dependencies 的 pom 文件,可以找到此文件中也引入了 JAR 依赖并使用变量指定了其版本,这里就是导致问题出现的主要原因;
        • —— A 模块引入 JAR 并指定了版本,但是传递给 B 模块时并不会传递版本号,就相当于 B 模块引入了未指定版本的 JAR,此时 maven 会优先在 B 的父依赖中寻找当前依赖的版本号,如果存在则按照此版本号去仓库下载;如果未找到则直接指定版本号为 RELEASE 然后去 maven 仓库下载;总结为 maven 下载依赖一定会按照版本号去下载;
      • 问题解决方式:由于 B 模块是直接引入 A 模块顺带将 JAR 引入,那么只需要指定 JAR 的版本和 A 模块保持一致就可以了,有如下方式:
        • —— 使用 excludes 去除 A 中 JAR 的依赖,在 B 中重新引入 JAR 的依赖并指定版本(不推荐)
        • —— 直接重新引入指定版本的 JAR 依赖,写在依赖集的最上方,maven 会按照最短路径引入此依赖,后面再引入 A 中 JAR 依赖时会以依赖冲突的方式去除掉(不推荐)
        • —— 找到 spring-boot-dependencies 中 JAR 依赖的版本变量,然后在 A 模块中定义相同的变量并指定想要的版本号,同时在 B 模块中做同样的操作即可(推荐)
      • 总结:解决依赖传递出现版本不一致的问题只需要在需要引入依赖的模块中覆盖掉默认版本号即可,默认版本号统一定义在 spring-boot-dependencies 中
  4. pom中的 spring-boot-configuration-processor 依赖有什么作用?
    • 答:针对自定义属性参数配置如何让使用方在不阅读源码的前提下知道有哪些配置参数及其作用说明,此依赖就是解决此问题;引入此依赖本地打包模块,会在target文件夹下的META-INF文件夹下生成spring-configuration-metadata.json文件,将此文件复制到工程的resources下的META-INF文件夹下即可实现参数自动提示(仅限 IDEA 编辑器,其他编辑器不了解);