背景
本地开发使用二方包里的feignclient时,本地调用远程服务超时(有墙等不可抗力),但是可以通过走网关的方式调用远程服务,所以修改(间接和直接)二方包里的feignclient以通过走网关调用远程服务。
前置知识,feignclient里指定了url时,优先通过url访问,而不是通过服务发现调用远程服务。
二方包提供的feignclient
@FeignClient(name = "test",
configuration = configuration.class)
interface OldClient {
@RequestMapping("/test")
String test();
}
方法一
使用方式:
继承OldClient,复制旧feignclient的参数,手动指定服务的网关地址url。
@FeignClient(name = "test",
url = "www.baidu.com",
configuration = configuration.class)
interface NewClient extends OldClient {
}
@FeignClient(name = "test",
url = "${local-gateway.test:}",
configuration = configuration.class)
interface NewClient extends OldClient {
}
${local-gateway.test:}
通过spel表达式,读取local-gateway.test
将local-gateway.test配置到application-local.yml等配置文件中。
只在local环境启动时,能读取到local-gateway.test,通过走网关的方式调用服务。在其他环境时,spel读取不到值,会取缺省值也就是空字符串,也与@FeignClient源码里中url()提供的缺省值一致。此时,会通过服务发现调用远程服务。
注意事项:
开发迭代时,要检查二方包中的client与我们自定义的client中的@FeignClient参数是否一致(一般二方包提供的client不会随意改变参数,迭代的时候检查一下是最保险的)。
(二方包提供者能通过spel的方式指定client的url就方便了。当然,本地能直接调用远程服务的话就没这个文章了)
(虽然FeignClient上有@Inherited注解,但是接口间不能继承注解,所以具体有什么作用暂时不知道,有知道的也请留言)
方法二
使用方式:
注册FeignClient时,修改设置url的代码,设置成自定义的地址。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
找到@EnableFeignClients,然后找到@Import(FeignClientsRegistrar.class)。
然后找到FeignClientsRegistrar.class。
前置知识:spring的Import相关知识可以先百度学习一下。
private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata,
Map<String, Object> attributes) {
String className = annotationMetadata.getClassName();
Class clazz = ClassUtils.resolveClassName(className, null);
ConfigurableBeanFactory beanFactory = registry instanceof ConfigurableBeanFactory
? (ConfigurableBeanFactory) registry : null;
String contextId = getContextId(beanFactory, attributes);
String name = getName(attributes);
FeignClientFactoryBean factoryBean = new FeignClientFactoryBean();
factoryBean.setBeanFactory(beanFactory);
factoryBean.setName(name);
factoryBean.setContextId(contextId);
factoryBean.setType(clazz);
factoryBean.setRefreshableClient(isClientRefreshEnabled());
BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(clazz, () -> {
factoryBean.setUrl(getUrl(beanFactory, attributes));
factoryBean.setPath(getPath(beanFactory, attributes));
factoryBean.setDecode404(Boolean.parseBoolean(String.valueOf(attributes.get("decode404"))));
Object fallback = attributes.get("fallback");
if (fallback != null) {
找到registerFeignClient方法,找到第16行
factoryBean.setUrl(getUrl(beanFactory, attributes));
这行代码就是设置url的逻辑。
因为FeignClientsRegistrar.class不能继承,所以自定义一个@EnableFeignClients和自定义一个FeignClientsRegistrar.class,在设置url时改写成自己的逻辑。将自定义FeignClientsRegistrar.class里与@EnableFeignClients相关的代码替换成自定义的@EnableFeignClients。
最后在启动类上使用自定义的@EnableFeignClients。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar2.class)
public @interface EnableCustomFeign {
factoryBean.setContextId(contextId);
factoryBean.setType(clazz);
factoryBean.setRefreshableClient(isClientRefreshEnabled());
BeanDefinitionBuilder definition =
BeanDefinitionBuilder.genericBeanDefinition(clazz, () -> {
if ("local".equals(SpringUtil.getActiveProfile())) {
factoryBean.setUrl("www.baidu.com");
} else {
factoryBean.setUrl(getUrl(beanFactory, attributes));
}
factoryBean.setPath(getPath(beanFactory, attributes));
使用方式与旧的@EnableFeignClients一致。
注意事项:
FeignClient相关的依赖版本变动时,需要检查我们重写后的类与版本变动后的类是否一致
方法三
使用方式:
注册feignclient之前,修改feignclient注解的参数。
前置知识:
前置知识:通过反射动态修改自定义注解属性值_反射修改注解的值_着实着迷的博客-CSDN博客
参考代码:org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider#scanCandidateComponents
由于spring是通过直接读取class文件,获取元数据,所以通过反射修改参数不可行。把我们的思路转移到直接修改class文件。
通过asm提供的库修改字节码。
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
ServletContextResourcePatternResolver servletContextResourcePatternResolver = new ServletContextResourcePatternResolver(resolver);
Resource[] resources1 = servletContextResourcePatternResolver.getResources("classpath*:com/example/LocalCodeApplication$xxx.class");
Resource resource = resources1[0];
ClassReader classReader = new ClassReader(resource.getInputStream());
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS);
ClassVisitor classVisitor = new ClassVisitor(Opcodes.ASM5, classWriter) {
@Override
public AnnotationVisitor visitAnnotation(String s, boolean b) {
AnnotationVisitor annotationVisitor = super.visitAnnotation(s, b);
if (s.equals("Lorg/springframework/cloud/openfeign/FeignClient;")) {
return new AnnotationVisitor(Opcodes.ASM5, annotationVisitor) {
@Override
public void visit(String s, Object o) {
if (s.equals("url")) {
super.visit(s, "www.360.com");
} else {
super.visit(s, o);
}
}
};
}
return annotationVisitor;
}
};
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
byte[] byteArray = classWriter.toByteArray();
FileUtil.writeBytes(byteArray, resource.getURI().getPath());