Spring 的 AOP 和配置优先级


AOP切入Spring Cloud Feign组件失败的情况

1. 为方便统一处理Feign,用AOP实现使用within指示器匹配feign.Client接口的实现进行AOP切入
//测试Feign
@FeignClient(name = "client")
public interface Client {
    @GetMapping("/feignaop/server")
    String api();
}

//AOP切入feign.Client的实现
@Aspect
@Slf4j
@Component
public class WrongAspect {
    @Before("within(feign.Client+)")
    public void before(JoinPoint pjp) {
        log.info("within(feign.Client+) pjp {}, args:{}", pjp, pjp.getArgs());
    }
}

//配置扫描Feign
@Configuration
@EnableFeignClients(basePackages = "org.geekbang.time.commonmistakes.spring.demo4.feign")
public class Config {
}
2. 通过feign调用服务查看日志实现feign.Client的切入,切入的是execute方法
3. 由客户端,让Ribbon来负载均衡,改为后端服务通过Nginx实现服务端负载均衡,设置URL属性,通过固定URL调用后端服务
@FeignClient(name = "anotherClient",url = "http://localhost:45678")
public interface ClientWithUrl {
    @GetMapping("/feignaop/server")
    String api();
}
4. AOP切面失效,within(feign.Client+)无法切入ClientWithUrl的调用;
5. 定义两个方法通过Client和ClientWithUrl两个Feign进行接口接口调用;
@Autowired
private Client client;

@Autowired
private ClientWithUrl clientWithUrl;

@GetMapping("client")
public String client() {
    return client.api();
}

@GetMapping("clientWithUrl")
public String clientWithUrl() {
    return clientWithUrl.api();
}
6. 调用Client后AOP有日志输出,调用ClientWithUrl没有;
7. 分析FeignClient创建过程,即分析FeignClientFactoryBean类的getTarget方法
8. 源码中判断URL为空或不配置调用loadBalance方法,在其内部通过FeignContext从容器获取feign.Client的实例;
 T getTarget() {
  FeignContext context = this.applicationContext.getBean(FeignContext.class);
  Feign.Builder builder = feign(context);
  if (!StringUtils.hasText(this.url)) {
    ...
    return (T) loadBalance(builder, context,
        new HardCodedTarget<>(this.type, this.name, this.url));
  }
  ...
  String url = this.url + cleanPath();
  Client client = getOptional(context, Client.class);
  if (client != null) {
    if (client instanceof LoadBalancerFeignClient) {
      // not load balancing because we have a url,
      // but ribbon is on the classpath, so unwrap
      client = ((LoadBalancerFeignClient) client).getDelegate();
    }
    builder.client(client);
  }
  ...
}
protected  T loadBalance(Feign.Builder builder, FeignContext context,
    HardCodedTarget target) {
  Client client = getOptional(context, Client.class);
  if (client != null) {
    builder.client(client);
    Targeter targeter = get(context, Targeter.class);
    return targeter.target(this, builder, context, target);
  }
...
}
protected  T getOptional(FeignContext context, Class type) {
  return context.getInstance(this.contextId, type);
}
9. debug看出client是LoadBalanceFeignClient已经是经过代理增强的,明显是一个Bean;
10. 没有指定URL的@FeignClient对应的LoadBalanceFeignClient是可以通过Feign.Client切入的。
11. 从源码中看到当URL不为空,client设置为LoadBalanceFeignClient的delegate属性。因为URL不需要客户端负载均衡了。
12. 但因Ribbon在classpath中,需要从LoadBalanceFeignClient提取真正Client,debug看出client是一个ApacheHttpClient;
13. HttpClientFeignLoadBalancedConfiguration源码中,LoadBalancerFeignClient这个Bean在实例化的时候,new出一个ApacheHttpClient作为delegate放入到LoadBalancerFeignClient中;

@Bean
@ConditionalOnMissingBean(Client.class)
public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
      SpringClientFactory clientFactory, HttpClient httpClient) {
   ApacheHttpClient delegate = new ApacheHttpClient(httpClient);
   return new LoadBalancerFeignClient(delegate, cachingFactory, clientFactory);
}

public LoadBalancerFeignClient(Client delegate,
      CachingSpringLoadBalancerFactory lbClientFactory,
      SpringClientFactory clientFactory) {
   this.delegate = delegate;
   this.lbClientFactory = lbClientFactory;
   this.clientFactory = clientFactory;
}
14. 因此ApacheHttpClient是new出来的不是Bean,而LoadBalanceFeignClient是一个Bean;
15. 为什么within(feign.Client+)无法切入设置过URL的@FeignClient ClientWithUrl?
 15.1 表达式声明的是切入feign.Client的实现类;
 15.2 Spring只能切入由自己管理的Bean;
 15.3 虽然LoadBalancerFeignClient和ApacheHttpClient都是feign.Client接口的实现,但是HttpClientFeignLoadBalancedConfiguration 的自动配置只是把前者定义为 Bean,后者是 new 出来的、作为了 LoadBalancerFeignClient 的 delegate,不是 Bean。
 15.4 在定义了FeignClient的URL属性后,我们获取到的是LoadBalancerFeignClient的delegate,不是Bean。
 15.5 因此定义了URL的FeignClient采用within(feign.Client+)无法切入;
16. 通过修改切点表达式,@FeignClient注解切,切的是ClientWithUrl接口的API方法,而不是client.Feign接口的execute方法不符合预期;
@Before("@within(org.springframework.cloud.openfeign.FeignClient)")
public void before(JoinPoint pjp){
    log.info("@within(org.springframework.cloud.openfeign.FeignClient) pjp {}, args:{}", pjp, pjp.getArgs());
}
17. 上述方式不是希望切的对象@FeignClient注解标在Feign Client接口上,切的是Feign定义的接口,而通过feign.Client接口切的是客户端实现类,切到的是通用的、执行所有Feign调用的execute方法;
18. ApacheHttpClient不是Bean无法切入,切Feign接口不符合要求;但ApachehttpClient有机会独立成为Bean;
19. 查看HttpClientFeignConfiguration源码,当没有ILoadBalancer类型的时候,自动装配会把ApacheHttpClient设置为Bean.
20. 这么做原因,如果我们不希望做客户端负载均衡的话,应该不会引用Ribbon组件的依赖,自如没有LoadBalancerFeignClient,只有ApacheHttpClient;
@Configuration
@ConditionalOnClass(ApacheHttpClient.class)
@ConditionalOnMissingClass("com.netflix.loadbalancer.ILoadBalancer")
@ConditionalOnMissingBean(CloseableHttpClient.class)
@ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
protected static class HttpClientFeignConfiguration {
  @Bean
  @ConditionalOnMissingBean(Client.class)
  public Client feignClient(HttpClient httpClient) {
    return new ApacheHttpClient(httpClient);
  }
}
21. 如果把pom.xml中ribbon模块注释掉,启动会报错,Cannot subclass final class feign.httpclient.AppcheHttpClient

  org.springframework.cloud
  spring-cloud-starter-netflix-ribbon

22. 这里涉及Spring实现动态代理的两种方式:
 22.1 JDK动态代理,通过反射实现,只支持对实现接口的类进行代理;
 22.2 CGLIB动态字节码注入方式,通过继承实现代理,没有限制;
23. Spring Boot 2.x默认使用CGLIB方式,但通过继承实现代理有个问题是,无法继承final类,ApacheHttpClient类定义为了final:
public final class ApacheHttpClient implements Client {
24. 为解决这个问题,把配置参数proxy-target-class的值修改为false,切换到JDK动态代理方式:
spring.aop.proxy-target-class=false
25. 修改后执行clientWithUrl接口,通过within(feign.Client+)方式可以切入feign.Client子类了;
26. 因此,Spring Cloud使用了自动装配来根据依赖装配组件,组件是否成为Bean决定了AOP是否可以切入,在尝试通过AOP切入Spring Bean的时候需要注意;

Spring程序配置优先级问题

1. 通过配置文件实现参数配置,当两个不同的配置源包含相同的配置项时,其中一个配置项可能会被覆盖掉,出现配置失效的问题;
2.  Spring Boot通过设置management.server.port参数,暴露独立actuator管理端口,这样做安全、方便统一监控程序是否健康
management.server.port=45679
3. 运维同学在服务器上定义了两个环境变量MANAGEMENT_SERVER_IP 和 MANAGEMENT_SERVER_PORT,目的是方便监控 Agent 把监控数据上报到统一的管理服务上:
MANAGEMENT_SERVER_IP=192.168.0.2
MANAGEMENT_SERVER_PORT=12345
4. MANAGEMENT_SERVER_PORT覆盖了配置文件中的management.server.port,修改了应用程序本身的端口导致监控系统无法通过老的管理端口访问到应用,出现重新发布后,监控系统显示程序离线,但程序正常运行,只是actuator管理端口号改了不是定义的45679
5. 为了方便登录,页面上默认显示管理员用户名,配置文件中配置,但程序读取不是配置文件中定义的
user.name=defaultadminname
6. 通过代码查看Spring到底能读取到几个management.server.port和user.name配置项;
7. 想要查询Spring中所有的配置,需要以环境Environment接口为入口,Spring通过环境Environment抽象出Property和Profile:
 7.1 针对Property又抽象出各种PropertySource类代表配置源。一个环境下可能有多个配置源,每个配置源中有诸多配置项。在查询配置信息时,需要按照配置源优先级进行查询;
 7.2 Profile定义了场景的概念。通常,我们会定义类似 dev、test、stage 和 prod 等环境作为不同的 Profile,用于按照场景对 Bean 进行逻辑归属。同时,Profile 和配置文件也有关系,每个环境都有独立的配置文件,但我们只会激活某一个环境来生效特定环境的配置文件。
8. 对于非Web应用,Spring对于Environment接口的实现是StandardEnvironment类。
 8.1 通过 Spring 注入 StandardEnvironment 后循环 getPropertySources 获得的 PropertySource;
 8.2 查询所有的 PropertySource 中 key 是 user.name 或 management.server.port 的属性值;
 8.3 然后遍历 getPropertySources 方法,获得所有配置源并打印出来:
@Autowired
private StandardEnvironment env;
@PostConstruct
public void init(){
    Arrays.asList("user.name", "management.server.port").forEach(key -> {
         env.getPropertySources().forEach(propertySource -> {
                    if (propertySource.containsProperty(key)) {
                        log.info("{} -> {} 实际取值:{}", propertySource, propertySource.getProperty(key), env.getProperty(key));
                    }
                });
    });

    System.out.println("配置优先级:");
    env.getPropertySources().stream().forEach(System.out::println);
}
9. 有三处定义了 user.name:第一个是 configurationProperties;第二个是 systemProperties,代表系统配置;第三个是 applicationConfig,也就是我们的配置文件,值是配置文件中定义的 defaultadminname。
 9.1 同样地,也有三处定义了 management.server.port:第一个是 configurationProperties;第二个是 systemEnvironment 代表系统环境;第三个是 applicationConfig,也就是我们的配置文件
 9.2 输出显示,Spring 中有 9 个配置源,值得关注是 ConfigurationPropertySourcesPropertySource、PropertiesPropertySource、OriginAwareSystemEnvironmentPropertySource 和我们的配置文件。

配置优先级:ConfigurationPropertySourcesPropertySource {name='configurationProperties'}StubPropertySource {name='servletConfigInitParams'}ServletContextPropertySource {name='servletContextInitParams'}PropertiesPropertySource {name='systemProperties'}OriginAwareSystemEnvironmentPropertySource {name='systemEnvironment'}RandomValuePropertySource {name='random'}OriginTrackedMapPropertySource {name='applicationConfig: [classpath:/application.properties]'}MapPropertySource {name='springCloudClientHostInfo'}MapPropertySource {name='defaultProperties'}
10. Spring 真的是按这个顺序查询配置吗?最前面的 configurationProperties,又是什么?则需要分析源码
11. 在分析Spring源码时,看到的表象不一定是实际运行时的情况还需要借助日志或调试工具理清整个过程;(Arthas工具可以分析代码调用路径)

Spring AOP支持10种切点指示符

execution、within、@within、@annotation、this、target、args、@target、@args
execution: 用来匹配执行方法的连接点的指示符。
用法相对复杂,格式如下:execution(权限访问符 返回值类型 方法所属的类名包路径.方法名(形参类型) 异常类型)
e.g. execution(public String com.darren.hellxz.test.Test.access(String,String))

权限修饰符和异常类型可省略,返回类型支持通配符,类名、方法名支持*通配,方法形参支持..通配
within: 用来限定连接点属于某个确定类型的类。
within(com.darren.hellxz.test.Test)
within(com.darren.hellxz.test.) //包下类
within(com.darren.hellxz.test..) //包下及子包下

this和target: this用于没有实现接口的Cglib代理类型,target用于实现了接口的JDK代理目标类型
举例:this(com.darren.hellxz.test.Foo) //Foo没有实现接口,使用Cglib代理,用this
实现了个接口public class Foo implements Bar{...}
target(com.darren.hellxz.test.Test) //Foo实现了接口的情况

args: 对连接点的参数类型进行限制,要求参数类型是指定类型的实例。
args(Long)

@target: 用于匹配类头有指定注解的连接点
@target(org.springframework.stereotype.Repository)

@args: 用来匹配连接点的参数的,@args指出连接点在运行时传过来的参数的类必须要有指定的注解

@Pointcut("@args(org.springframework.web.bind.annotation.RequestBody)")
public void methodsAcceptingEntities() {}

@within: 指定匹配必须包括某个注解的的类里的所有连接点
@within(org.springframework.stereotype.Repository)

@annotation: 匹配那些有指定注解的连接点
@annotation(org.springframework.stereotype.Repository)

bean: 用于匹配指定Bean实例内的连接点,传入bean的id或name,支持使用*通配符

切点表达式组合
使用&&、||、!、三种运算符来组合切点表达式,表示与或非的关系

相关