Feign配置了全局日志后局部日志失效

前言

这个问题是最近做微服务架构设计的时候遇到的,项目采用SpringCloud这套,那么服务间RPC调用就采用OpenFeign了,后期可能需要OpenFeign进行一些优化,所以深入了一下OpenFeign的源码,在调试Feign源码的时候配置了一下Feign服务间调用的日志这块,也就是配置Feign服务调用的日志输出内容,说道Feign的配置,分为配置方式(通过配置文件yml、通过java代码配置类)、配置类型(全局配置、局部配置),本文不讲具体的配置方式,只是演示基于java配置类配置Feign日志输出内容全局配置后局部配置失败!当然还将讲解为什么会失败!

示例代码

完整项目目录
在这里插入图片描述

依赖关系

	=======================父pom=======================
	<properties>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <spring-boot.version>2.3.7.RELEASE</spring-boot.version>
        <spring-cloud-alibaba.version>2.2.2.RELEASE</spring-cloud-alibaba.version>
        <spring-cloud.version>Hoxton.SR9</spring-cloud.version>
    </properties>
 	<dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.22</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- 添加 Nacos 支持 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${spring-cloud-alibaba.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

=====================provider-api-pom===================
	<dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
    </dependencies>

=====================consumer-pom=======================
	<dependencies>
        <dependency>
            <groupId>com.tao</groupId>
            <artifactId>provider-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>


基本代码
服务提供者-provider-server

//主启动
@EnableDiscoveryClient
@SpringBootApplication
public class ProviderApplication {
    public static void main(String[] args) {
		SpringApplication.run(ProviderApplication.class, args);
    }
}

//服务提供者1
@Slf4j
@RestController
public class Provider1Controller {
    @RequestMapping("/call1/{name}")
    public String call1(@PathVariable String name)  {
        log.info("服务提供者1被调用--name==》"+name);
        return LocalTime.now() + "——服务提供者1:" + name;
    }
}

//服务提供者2
@Slf4j
@RestController
public class Provider2Controller {
    @RequestMapping("/call2/{name}")
    public String call1(@PathVariable String name)  {
        log.info("服务提供者2被调用--name==》"+name);
        return LocalTime.now() + "——服务提供者2:" + name;
    }
}

服务提供者对外暴露接口-provider-api

//Provider1Controller-FeignClient
@FeignClient(contextId="remoteProvider1Service", value="provider")
public interface RemoteProvider1Service {
    @GetMapping("/call1/{name}")
    String call1(@PathVariable(value = "name") String name);
}

//Provider2Controller-FeignClient
@FeignClient(contextId="remoteProvider2Service", value="provider")
public interface RemoteProvider2Service {
    @GetMapping("/call2/{name}")
    String call2(@PathVariable(value = "name") String name);
}

服务消费者

@EnableDiscoveryClient
@SpringBootApplication
@EnableFeignClients(value = "com.tao" )
public class ConsumerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConsumerApplication.class, args);
    }
}

@Slf4j
@RestController
public class Consumer1Controller {
    @Resource
    private RemoteProvider1Service remoteProvider1Service;

    @GetMapping("/call1")
    public String consumer1(@RequestParam String name) {
        return remoteProvider1Service.call1(name);
    }
}

@Slf4j
@RestController
public class Consumer2Controller {
    @Resource
    private RemoteProvider2Service remoteProvider2Service;

    @GetMapping("/call2")
    public String consumer2(@RequestParam String name) {
        return remoteProvider1Service.call2(name);
    }
}


基于上述代码已经可以实现最基本的通过Feign远程调用
在这里插入图片描述
在这里插入图片描述

添加Feign日志输出配置

上面说了,只演示基于Java代码配置,不做配置文的配置!先添加局部配置,在配置全局配置,局部配置是可以独立控制每个Feign客户端的日志输出格式,全局的就是在@EnableFeignClients中配置,所有的FeignClient都生效

局部配置

独立控制RemoteProvider1Service、和RemoteProvider2Service的日志输出内容!
添加日志输出内容BASIC、FULL
关于内容输出类型这里不讲,自己去看其他文章!

public class FeignLevelConfigurationBASIC {
    @Bean
    public Logger.Level logLevelBASIC(){
    	//没别的意思,方便后面调试查看
        System.out.println("logLevelBASIC");
        return Logger.Level.BASIC;
    }
}

public class FeignLevelConfigurationFULL {
    @Bean
    public Logger.Level logLevelFULL(){
        System.out.println("logLevelFULL");
        return Logger.Level.FULL;
    }
}

配置Feign客户端日志配置
也就是更改原有的RemoteProvider1Service、RemoteProvider2Service的@FeignClient注解配置,添加configuration即可!

@FeignClient(contextId="remoteProvider1Service", value="provider",configuration = FeignLevelConfigurationFULL.class)

@FeignClient(contextId="remoteProvider2Service", value="provider",configuration = FeignLevelConfigurationBASIC.class)

重启服务消费者
在这里插入图片描述
那么我们调用remoteProvider1Service中的接口,就会输出日志内容为FULL类型,如果调用remoteProvider2Service中的接口,就会输出日志内容为BASIC类型!

全局配置

剔除局部配置
先剔除掉RemoteProvider1Service、RemoteProvider2Service接口上@FeignClient注解中的configuration配置,剔除后如下!

@FeignClient(contextId="remoteProvider1Service", value="provider")
public interface RemoteProvider1Service {
	//......
}

@FeignClient(contextId="remoteProvider2Service", value="provider")
public interface RemoteProvider2Service {
	//......
}

注意:config包中的FeignLevelConfigurationBASIC、和FeignLevelConfigurationFULL还是保留!这里只是将@FeignClient注解中的configuration配置剔除!

配置全局配置
也就是配置EnableFeignClients注解的defaultConfiguration的值

@EnableDiscoveryClient
@SpringBootApplication
@EnableFeignClients(value = "com.tao" ,defaultConfiguration = FeignLevelConfigurationBASIC.class)
public class ConsumerApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext run = SpringApplication.run(ConsumerApplication.class, args);
    }

}

重启服务消费者
在这里插入图片描述
那么这里即便没有给独立的Feign客户端配置日志输出类型,在全局使用@EnableFeignClients注解的defaultConfiguration配置也是全局生效的!文章到这里其实都还没进入主题,上面都是做铺垫!接下来才是本文要演示的问题,以及解释!

配置全局日志和局部日志

为什么这么配置,其实就是想达到我们给RemoteProvider1Service配置FULL类型日志输出,RemoteProvider2Service不配置日志输出类型,在@EnableFeignClients注解中配置defaultConfiguration配置日志输出类型为BASIC,让RemoteProvider2Service走全局默认配置,代码改造如下!

主启动类

配置@EnableFeignClients的defaultConfiguration为FeignLevelConfigurationBASIC.class,作为全局默认配置!

@EnableDiscoveryClient
@SpringBootApplication
@EnableFeignClients(value = "com.tao",defaultConfiguration = FeignLevelConfigurationBASIC.class)
public class ConsumerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConsumerApplication.class, args);
    }
}

RemoteProvider1Service

这里配置RemoteProvider1Service的@FeignClient的configuration为FeignLevelConfigurationFULL,就是想让RemoteProvider1Service日志输出类型为FULL

@FeignClient(contextId="remoteProviderService", value="provider",configuration = FeignLevelConfigurationFULL.class)
public interface RemoteProvider1Service {

    @GetMapping("/call1/{name}")
    String call1(@PathVariable(value = "name") String name);

}

RemoteProvider2Service

RemoteProvider2Service的@FeignClient的configuration不做任何配置,让RemoteProvider2Service走全局默认配置!

@FeignClient(contextId="remoteProvider2Service", value="provider")
public interface RemoteProvider2Service {

    @GetMapping("/call2/{name}")
    String call2(@PathVariable(value = "name") String name);

}

想要达到的结果

当我们调用RemoteProvider1Service中的接口时,输出日志内容为FULL,应为RemoteProvider1Service独立配置了日志输出内容为FULL,调用RemoteProvider2Service中的接口时,输出日志内容为BASIC,因为RemoteProvider2Service中没有独立配置日志输出类型,所以会应用全局默认配置,也就是@EnableFeignClients中配置的defaultConfiguration中配置的日志输出类型BASIC

实际效果

在这里插入图片描述
当我们调用http:///call1/tao这个的时候我们知道走的是RemoteProvider1Service,但是这里并没有输出日志内容为FULL,而是NONE,当我们调用http:///call2/tao的时候是调用的RemoteProvider2Service中的接口,它输出的内容是BASIC,就算RemoteProvider1Service使用FULL独立配置失败应该也可以使用@EnableFeignClients中配置的defaultConfiguration中配置的日志输出类型BASIC,但是并没有,而是直接使用的Feign默认日志输出类型NONE

问题原因

这个问题需要去看一下Feign的源码,就是我们在使用Feign的时候,通过@EnableFeignClients帮我们注册Feign客户端,在这个注册Feign部分其实住注册两块东西,一部分是注册客户端配置,另一部分就是真正注册Feign客户端,就是注册配置的时候,又分为全局配置和独立Feign配置,其中全局配置其实不管我们有没有在@EnableFeignClients的defaultConfiguration配置值,都会为我们在Spring的BeanDefinitionMap中注册一个默认的配置,代码如下!
在这里插入图片描述
注册完默认配置后,就会调用registerFeignClients(metadata, registry);方法,这个方法中不但会注册Feign客户端,还会注册Feign客户端的配置,代码如下
在这里插入图片描述
registerClientConfiguration方法就是注册客户端配置的,registerFeignClient就是注册Feign客户端的!那么我们按照我们上面,配置全局日志和局部日志部分配置代码,我们在Spring的BeanDefinitionMap中就存在三个Feign配置Bean,一个是默认的,另外两个是Feign客户端的,就算Feign客户端在@FeignClient注解中没有配置configuration,这个Feign客户端配置Bean也是会构建存入Spring的BeanDefinitionMap中的!然后我们这里使用的是SpringBoot,那么feign的源码中的spring.factorice文件中会配置FeignAutoConfiguration
在这里插入图片描述
FeignAutoConfiguration这个类最终会被SpringBoot装载到Spring的容器中
在这里插入图片描述
那么private List configurations = new ArrayList<>();中就会收集Spring中Bean类型为FeignClientSpecification的Bean装载到configurations中,当我们在构建FeignContext上下文的时候,就会将FeignAutoConfiguration中的configurations装配到FeignContext的父类NamedContextFactory的configurations中
在这里插入图片描述
然后每个Feign客户端构建自己的代理对象的时候都会通过NamedContextFactory创建当前Feign客户端的子容器,
在这里插入图片描述
这里第一个是获取当前客户端的配置,注册到子容器AnnotationConfigApplicationContext的BeanDefinitionMap中,第二个是获取一default.开头的默认配置到BeanDefinitionMap,那么问题其实就出现在这里,我们在上面讲解到NamedContextFactorythis.configurations是在创建FeignContext的时候初始化的,具体值如下!
在这里插入图片描述
当我们创建remoteProviderService子容器的时候第一个判断我们能获取到值FeignLevelConfigurationFULL添加到当前子容器的BeanDefinitionMap,第二个取默认的default.也可以获取到FeignLevelConfigurationBASIC添加到当前子容器的BeanDefinitionMap中,那么最后调用context.refresh();执行完后当前remoteProviderService子容器中就有两个Logger.Level类型的logLevelFULL、logLevelBASIC;

当我们创建remoteProvider2Service子容器的时候第一个判断我们取到的configuration中的值数组是0,,因为remoteProvider2Service的configuration没配置,第二个取默认的default.还是可以获取像上面一样的FeignLevelConfigurationBASIC,那么最后调用context.refresh();执行完后当前remoteProviderService子容器中就有一个个Logger.Level类型logLevelBASIC;
在这里插入图片描述
虽然这里不报错,但是我们在构建FeignClient的builder的时候通过Logger.Level.class去当前remoteProviderService的子容器getBean的时候就会报错,
在这里插入图片描述
但是被内部catch掉了,返回null
在这里插入图片描述
报错提示是通过Logger.Level找到了两个,Spring不知道取哪一个,然后又被catch掉了返回null
在这里插入图片描述
返回null不更新logLevel的值,logLevel的值默认是NONE
在这里插入图片描述
在这里插入图片描述
所以这也就导致了实际结果与我们想要达到的结果不一样的问题!网上查阅了其他文章,给出的解释:是什么日志配置类存放的包和主启动类在一起,然后被扫到、还有是说存在两个Logger.Level后面的会被覆盖掉等,净扯淡!!!核心问题就是Feign客户端子容器中存在多个Logger.Level类型的Bean,通过Logger.Level类型取出的时候Spring不知道取那个,导致报错,cache捕获后返回null,然后就使用Level的默认级别NONE了!!!

解决方案

方式1

代码改动

在没深入源码的时候是直接在全局日志输出类型配置做处理,去掉主启动类上@EnableFeignClients上的defaultConfiguration值

@EnableDiscoveryClient
@SpringBootApplication
@EnableFeignClients(value = "com.tao" )
@ComponentScan("com.tao")
public class ConsumerApplication {
    public static void main(String[] args) {
         SpringApplication.run(ConsumerApplication.class, args);
    }
}

将默认配置类添加@Component,或者其他注解,只要能让Spring容器加载FeignLevelConfigurationBASIC就行,注意:我这里FeignLevelConfigurationBASIC和主启动类不在同一个包下,所以添加了@ComponentScan(“com.tao”)代码如下!

@Component
public class FeignLevelConfigurationBASIC {
    @Bean
    public Logger.Level logLevelBASIC(){
        System.out.println("logLevelBASIC");
        return Logger.Level.BASIC;
    }
}

重启服务
在这里插入图片描述
这里分别使用的是FULL和BASIC,满足我们的需求!

方式2

方式二是我们看了上面产生问题的原因后得知就是remoteProvider1Service子容器获取类型为 Logger.Level的Bean的时候发现有两个,不知道取哪一个,那么我们就给需要独立配置的那个Logger.Level的Bean设置为主要的,通过@Primary注解实现!代码恢复到 这部分配置全局日志和局部日志的代码即可,
主启动类

@EnableFeignClients(value = "com.tao" ,defaultConfiguration = FeignLevelConfigurationBASIC.class)

FeignLevelConfigurationFULL

public class FeignLevelConfigurationFULL {
    @Bean
    @Primary
    public Logger.Level logLevelFULL(){
        System.out.println("logLevelFULL");
        return Logger.Level.FULL;
    }
}

FeignLevelConfigurationBASIC

public class FeignLevelConfigurationBASIC {
    @Bean
    public Logger.Level logLevelBASIC(){
        System.out.println("logLevelBASIC");
        return Logger.Level.BASIC;
    }
}

重启服务
在这里插入图片描述


版权声明:本文为CSDN877425287原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。