Introduction
So we have a starter that we are using as a parent, and this has various Templates and autoconfigurations that load feign clients as well.
The feign clients are using eureka discovery underneath in order to iterate through the services.
So here is an example of one of our feign clients:
import org.springframework.cloud.openfeign.FeignClient;
@FeignClient(name="decide-proxy")
public interface DecideControllerApiClient extends DecideControllerApi {
}
In the projects that are using this spring boot ( and also cloud starter ) as a parent ( in our pom.xml), I would like to be able to go to the properties file and do stuff like this:
eureka.client.filter.enabled=true
eureka.client.filter.services.doc-tools.host=localhost
eureka.client.filter.services.doc-tools.port=8015
I have a way to do this now, but it involves using Aspects, and reflection, and it seems to slow down my projects - it's a big ugly hack.
So is there a way to possibly do it through a ribbon property or something else?
Without modifying the starter code preferably ?
Question is: How can we change feign client returned instance by using property file. Essentially how can we over-ride starter and eureka auto ribbon behavior.
Current Hack
So I saw the following link: https://stackoverflow.com/a/42413801/1688441
And based on that I created the following which seems to work:
import com.netflix.appinfo.InstanceInfo;
import eureka.InstanceBuildVersionProperties.InstanceHostFilter;
import lombok.RequiredArgsConstructor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.netflix.eureka.EurekaDiscoveryClient.EurekaServiceInstance;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@RequiredArgsConstructor
@Aspect
public class EurekaInstanceHostFilter {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private final String versionMetadataKey;
private final InstanceBuildVersionProperties filters;
@SuppressWarnings("unchecked")
@Around("execution(public * org.springframework.cloud.netflix.eureka.EurekaDiscoveryClient.getInstances(..))")
public Object filterInstances(ProceedingJoinPoint jp) throws Throwable {
if (filters == null || !filters.isEnabled()) logger.error("Should not be filtering...");
List<ServiceInstance> instances = (List<ServiceInstance>) jp.proceed();
String serviceId = (String) jp.getArgs()[0];
InstanceHostFilter filter = filters.getServices().get(serviceId);
if(filter != null){
instances.forEach( instance -> {
try {
Class<?> clazz = EurekaServiceInstance.class;
Class<?> clazzInfo = InstanceInfo.class;
Field instanceField = clazz.getDeclaredField("instance");
instanceField.setAccessible(true);
InstanceInfo instanceInfo = (InstanceInfo) instanceField.get(instance);
String originalHostName =instanceInfo.getHostName();
int originalPort =instanceInfo.getPort();
//SET THE VALUES
String changeInstanceId = filter.getHost() + ":" + instance.getServiceId() + ":" +filter.getPort();
setField(instanceInfo, clazzInfo, "instanceId", changeInstanceId );
//HomePageURL
String newHomePageUrl = instanceInfo.getHomePageUrl().replace(originalHostName, filter.getHost()) .replace(originalPort+"", filter.getPort()+"");
setField(instanceInfo, clazzInfo, "homePageUrl", newHomePageUrl );
//StatusPageUrl
String statusPageUrl = instanceInfo.getStatusPageUrl().replace(originalHostName, filter.getHost()) .replace(originalPort+"", filter.getPort()+"");
setField(instanceInfo, clazzInfo, "statusPageUrl", statusPageUrl );
//healthCheckURL
String healthCheckUrl = instanceInfo.getHealthCheckUrl().replace(originalHostName, filter.getHost()) .replace(originalPort+"", filter.getPort()+"");
setField(instanceInfo, clazzInfo, "healthCheckUrl", healthCheckUrl );
//hostName
String hostName = instanceInfo.getHostName().replace(originalHostName, filter.getHost());
setField(instanceInfo, clazzInfo, "hostName", hostName );
setIntField(instanceInfo, clazzInfo, "port", filter.getPort());
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
});
}
return instances;
}
private void setField(Object instanceInfo, Class<?> clazzInfo, String fieldName, String value) throws NoSuchFieldException, IllegalAccessException {
Field instanceId = clazzInfo.getDeclaredField(fieldName);
instanceId.setAccessible(true);
instanceId.set(instanceInfo, value);
}
private void setIntField(Object instanceInfo, Class<?> clazzInfo, String fieldName, int value) throws NoSuchFieldException, IllegalAccessException {
Field instanceId = clazzInfo.getDeclaredField(fieldName);
instanceId.setAccessible(true);
instanceId.setInt(instanceInfo, value);
}
}
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConditionalOnProperty(name = "eureka.client.filter.enabled", havingValue = "true")
@EnableConfigurationProperties(InstanceBuildVersionProperties.class)
public class EurekaInstanceHostFilterAutoConfig {
@Value("${eureka.instance.metadata.keys.version:instanceBuildVersion}")
private String versionMetadataKey;
@Bean
@ConditionalOnProperty(name = "eureka.client.filter.enabled", havingValue = "true")
public EurekaInstanceHostFilter eurekaInstanceBuildVersionFilter(InstanceBuildVersionProperties filters) {
return new EurekaInstanceHostFilter(versionMetadataKey, filters);
}
}
import lombok.Getter;
import lombok.Setter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.HashMap;
import java.util.Map;
import static org.apache.commons.lang3.ArrayUtils.contains;
@ConfigurationProperties("eureka.client.filter")
public class InstanceBuildVersionProperties {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
/**
* Indicates whether or not service instances versions should be filtered
*/
@Getter @Setter
private boolean enabled = false;
/**
* Map of service instance version filters.
* The key is the service name and the value configures a filter set for services instances
*/
@Getter
private Map<String, InstanceHostFilter> services = new HashMap<>();
public boolean isKept(String serviceId) {
logger.debug("Considering service {} instance", serviceId);
if (services.containsKey(serviceId)) {
InstanceHostFilter filter = services.get(serviceId);
//TODO:
return true;
}
return true;
}
@Getter @Setter
public static class InstanceHostFilter {
/**
* host to use
*/
private String host;
private int port;
}
}
The above code I have noticed caused my project to slow down quite a bit. Additionally, it's very hacky due to using reflection and aspects.
Other Ideas
The only other idea is to maybe start playing with conditional loading of configurations within the starter itself. So, I could try to either load one feign client, or another feign client depending on the properties within the application property file.
Ofcourse this would probably mean that we would have to include the debugging support within each template, feign client, and auto configuration for each microservice.
Ideas?
Update
We tried using the properties file, and one of the ribbon properties plus feign name but this did not work. Something like:
decide-proxy.ribbon.listOfServers=