Hot questions for Spring Cloud Netflix

Top 10 Java Open Source / Spring / Spring Cloud Netflix

Question:

I have been using Zuul as the edge service and API Gateway. Recently I have noticed that Spring Cloud Platform release Spring Cloud Gateway. What is the difference between the two gateways? Why is the Zuul not extended to support the functionalities in S-C-Gateway? What was the driving factor for a new library altogether? When should it be used?


Answer:

I am the author of spring cloud gateway. Zuul is built on servlet 2.5 (works with 3.x), using blocking APIs. It doesn't support any long lived connections, like websockets.

Gateway is built on Spring Framework 5, Project Reactor and Spring Boot 2 using non-blocking APIs. Websockets are supported and it's a much better developer experience since it's tightly integrated with Spring.

Question:

Is it possible to set dynamic values to a header ?

@FeignClient(name="Simple-Gateway")
interface GatewayClient {
    @Headers("X-Auth-Token: {token}")
    @RequestMapping(method = RequestMethod.GET, value = "/gateway/test")
        String getSessionId(@Param("token") String token);
    }

Registering an implementation of RequestInterceptor adds the header but there is no way of setting the header value dynamically

@Bean
    public RequestInterceptor requestInterceptor() {

        return new RequestInterceptor() {

            @Override
            public void apply(RequestTemplate template) {

                template.header("X-Auth-Token", "some_token");
            }
        };
    } 

I found the following issue on github and one of the commenters (lpborges) was trying to do something similar using headers in @RequestMapping annotation.

https://github.com/spring-cloud/spring-cloud-netflix/issues/288

Kind Regards


Answer:

The solution is to use @RequestHeader annotation instead of feign specific annotations

@FeignClient(name="Simple-Gateway")
interface GatewayClient {    
    @RequestMapping(method = RequestMethod.GET, value = "/gateway/test")
    String getSessionId(@RequestHeader("X-Auth-Token") String token);
}

Question:

I understand @LoadBalanced indicates the Rest template should be based on Client Side Load Balancing using Ribbon and checks Eureka server for resolving the service name to host/port.

What is the use of @RibbonClient. Is it to support native Ribbon Client LB without Eureka and also support Eureka Discover when configured with DiscoveryEnabledNIWSServerList?


Answer:

TL;DR: @LoadBalanced is a marker annotation & @RibbonClient is used for configuration purposes.


@LoadBalanced

Used as a marker annotation indicating that the annotated RestTemplate should use a RibbonLoadBalancerClient for interacting with your service(s).

In turn, this allows you to use "logical identifiers" for the URLs you pass to the RestTemplate. These logical identifiers are typically the name of a service. For example:

restTemplate.getForObject("http://some-service-name/user/{id}", String.class, 1);

where some-service-name is the logical identifier.

@RibbonClient

Used for configuring your Ribbon client(s).

Is @RibbonClient required?

No! If you're using Service Discovery and you're ok with all of the default Ribbon settings, you don't even need to use the @RibbonClient annotation.

When should I use @RibbonClient?

There are at least two cases where you need to use @RibbonClient

  1. You need to customize your Ribbon settings for a particular Ribbon client
  2. You're not using any service discovery

Customizing your Ribbon settings:

Define a @RibbonClient

@RibbonClient(name = "some-service", configuration = SomeServiceConfig.class)
  • name - set it to the same name of the service you're calling with Ribbon but need additional customizations for how Ribbon interacts with that service.
  • configuration - set it to an @Configuration class with all of your customizations defined as @Beans. Make sure this class is not picked up by @ComponentScan otherwise it will override the defaults for ALL Ribbon clients.

See the section "Customizing the RibbonClient` in the Spring Cloud Netflix documentation (link)

Using Ribbon without Service Discovery

If you're not using Service Discovery, the name field of the @RibbonClient annotation will be used to prefix your configuration in the application.properties as well as "logical identifier" in the URL you pass to RestTemplate.

Define a @RibbonClient

@RibbonClient(name = "myservice")

then in your application.properties

myservice.ribbon.eureka.enabled=false
myservice.ribbon.listOfServers=http://localhost:5000, http://localhost:5001

Question:

I use Spring-Cloud-Netflix for communication between micro services. Let's say I have two services, Foo and Bar, and Foo consumes one of Bar's REST endpoints. I use an interface annotated with @FeignClient:

@FeignClient
public interface BarClient {
  @RequestMapping(value = "/some/url", method = "POST")
  void bazzle(@RequestBody BazzleRequest);
}

Then I have a service class SomeService in Foo, which calls the BarClient.

@Component
public class SomeService {
    @Autowired
    BarClient barClient;

    public String doSomething() {
      try {
        barClient.bazzle(new BazzleRequest(...));
        return "so bazzle my eyes dazzle";
      } catch(FeignException e) {
        return "Not bazzle today!";
      }

    }
}

Now, to make sure the communication between services works, I want to build a test that fires a real HTTP request against a fake Bar server, using something like WireMock. The test should make sure that feign correctly decodes the service response and reports it to SomeService.

public class SomeServiceIntegrationTest {

    @Autowired SomeService someService;

    @Test
    public void shouldSucceed() {
      stubFor(get(urlEqualTo("/some/url"))
        .willReturn(aResponse()
            .withStatus(204);

      String result = someService.doSomething();

      assertThat(result, is("so bazzle my eyes dazzle"));
    }

    @Test
    public void shouldFail() {
      stubFor(get(urlEqualTo("/some/url"))
        .willReturn(aResponse()
            .withStatus(404);

      String result = someService.doSomething();

      assertThat(result, is("Not bazzle today!"));
    }
}

How can I inject such a WireMock server into eureka, so that feign is able to find it and communicate with it? What kind of annotation magic do I need?


Answer:

Here is an example of using WireMock to test SpringBoot configuration with Feign client and Hystrix fallback.

If you are using Eureka as a server discovery, you need to disable it by setting a property "eureka.client.enabled=false".

First, we need to enable the Feign/Hystrix configuration for our application:

@SpringBootApplication
@EnableFeignClients
@EnableCircuitBreaker
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

@FeignClient(
        name = "bookstore-server",
        fallback = BookClientFallback.class,
        qualifier = "bookClient"
)
public interface BookClient {

    @RequestMapping(method = RequestMethod.GET, path = "/book/{id}")
    Book findById(@PathVariable("id") String id);
}

@Component
public class BookClientFallback implements BookClient {

    @Override
    public Book findById(String id) {
        return Book.builder().id("fallback-id").title("default").isbn("default").build();
    }
}

Please note that we are specifying a fallback class for the Feign client. Fallback class will be called every time Feign client call fails (e.g. connection timeout).

In order for tests to work, we need to configure the Ribbon loadbalancer (will be used internally by Feign client when sending http request):

@RunWith(SpringRunner.class)
@SpringBootTest(properties = {
        "feign.hystrix.enabled=true"
})
@ContextConfiguration(classes = {BookClientTest.LocalRibbonClientConfiguration.class})
public class BookClientTest {

    @Autowired
    public BookClient bookClient;

    @ClassRule
    public static WireMockClassRule wiremock = new WireMockClassRule(
            wireMockConfig().dynamicPort()));

    @Before
    public void setup() throws IOException {
        stubFor(get(urlEqualTo("/book/12345"))
                .willReturn(aResponse()
                        .withStatus(HttpStatus.OK.value())
                        .withHeader("Content-Type", MediaType.APPLICATION_JSON)
                        .withBody(StreamUtils.copyToString(getClass().getClassLoader().getResourceAsStream("fixtures/book.json"), Charset.defaultCharset()))));
    }

    @Test
    public void testFindById() {
        Book result = bookClient.findById("12345");

        assertNotNull("should not be null", result);
        assertThat(result.getId(), is("12345"));
    }

    @Test
    public void testFindByIdFallback() {
        stubFor(get(urlEqualTo("/book/12345"))
                .willReturn(aResponse().withFixedDelay(60000)));

        Book result = bookClient.findById("12345");

        assertNotNull("should not be null", result);
        assertThat(result.getId(), is("fallback-id"));
    }

    @TestConfiguration
    public static class LocalRibbonClientConfiguration {
        @Bean
        public ServerList<Server> ribbonServerList() {
            return new StaticServerList<>(new Server("localhost", wiremock.port()));
        }
    }
}

Ribbon server list need to match the url (host and port) of our WireMock configuration.

Question:

I'm trying to use Feign client. Below is my feing client:

import com.eprogrammerz.examples.domain.Movie;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

/**
 * Created by Yogen on 12/26/2016.
 */
@FeignClient(name = "movie-api")
public interface MovieApi {
    @RequestMapping(method = RequestMethod.GET, value = "/movies/{id}")
    Movie getMovie(@PathVariable("id") Long id);
}

I'm calling it from simple service as below:

@Service
public class MovieService {

    @Autowired
    MovieApi movieApi;

    public Movie findMovie(Long id){
        Movie movieOfTheDay = movieApi.getMovie(id);
        return movieOfTheDay;
    }
}

My spring boot app is as below:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.netflix.feign.EnableFeignClients;

@EnableFeignClients(basePackages = {"com.eprogrammerz.examples"})
@EnableCircuitBreaker
@SpringBootApplication
public class ClientAppApplication {

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

build.gradle

buildscript {
    ext {
        springBootVersion = '1.4.3.RELEASE'
    }
    repositories {
        jcenter()
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'

jar {
    baseName = 'client-app'
    version = '0.0.1-SNAPSHOT'
}
sourceCompatibility = 1.8
targetCompatibility = 1.8

repositories {
    jcenter()
    mavenCentral()
    maven { url "https://repo.spring.io/snapshot" }
    maven { url "https://repo.spring.io/milestone" }
}


dependencies {
    compile('org.springframework.cloud:spring-cloud-starter-feign')
    // https://mvnrepository.com/artifact/com.netflix.hystrix/hystrix-core
    compile('org.springframework.cloud:spring-cloud-starter-hystrix')
    compile("org.springframework.boot:spring-boot-starter-web"){
        exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'
    }
    compileOnly('org.projectlombok:lombok')
    testCompile('org.springframework.boot:spring-boot-starter-test')
}

dependencyManagement {
    imports {
        mavenBom "org.springframework.cloud:spring-cloud-dependencies:Camden.BUILD-SNAPSHOT"
        mavenBom "org.springframework.boot:spring-boot-starter-parent:${springBootVersion}"
    }
}

I'm getting error as below:

2016-12-30 13:07:16.894  INFO 6748 --- [nio-8082-exec-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization completed in 39 ms
2016-12-30 13:07:16.939  INFO 6748 --- [nio-8082-exec-1] c.e.e.controllers.RequestController      : Calling findMovie(1203)
2016-12-30 13:07:17.240  INFO 6748 --- [rix-movie-api-1] s.c.a.AnnotationConfigApplicationContext : Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@547496c2: startup date [Fri Dec 30 13:07:17 EST 2016]; parent: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@542e560f
2016-12-30 13:07:17.318  INFO 6748 --- [rix-movie-api-1] f.a.AutowiredAnnotationBeanPostProcessor : JSR-330 'javax.inject.Inject' annotation found and supported for autowiring
2016-12-30 13:07:17.619  INFO 6748 --- [rix-movie-api-1] c.netflix.config.ChainedDynamicProperty  : Flipping property: movie-api.ribbon.ActiveConnectionsLimit to use NEXT property: niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit = 2147483647
2016-12-30 13:07:17.670  INFO 6748 --- [rix-movie-api-1] c.n.u.concurrent.ShutdownEnabledTimer    : Shutdown hook installed for: NFLoadBalancer-PingTimer-movie-api
2016-12-30 13:07:17.727  INFO 6748 --- [rix-movie-api-1] c.netflix.loadbalancer.BaseLoadBalancer  : Client:movie-api instantiated a LoadBalancer:DynamicServerListLoadBalancer:{NFLoadBalancer:name=movie-api,current list of Servers=[],Load balancer stats=Zone stats: {},Server stats: []}ServerList:null
2016-12-30 13:07:17.739  INFO 6748 --- [rix-movie-api-1] c.n.l.DynamicServerListLoadBalancer      : Using serverListUpdater PollingServerListUpdater
2016-12-30 13:07:17.746  INFO 6748 --- [rix-movie-api-1] c.n.l.DynamicServerListLoadBalancer      : DynamicServerListLoadBalancer for client movie-api initialized: DynamicServerListLoadBalancer:{NFLoadBalancer:name=movie-api,current list of Servers=[],Load balancer stats=Zone stats: {},Server stats: []}ServerList:com.netflix.loadbalancer.ConfigurationBasedServerList@45bcfd5
2016-12-30 13:07:18.191 ERROR 6748 --- [nio-8082-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is com.netflix.hystrix.exception.HystrixRuntimeException: MovieApi#getMovie(Long) failed and no fallback available.] with root cause

com.netflix.client.ClientException: Load balancer does not have available server for client: movie-api
    at com.netflix.loadbalancer.LoadBalancerContext.getServerFromLoadBalancer(LoadBalancerContext.java:468) ~[ribbon-loadbalancer-2.2.0.jar:2.2.0]
    at com.netflix.loadbalancer.reactive.LoadBalancerCommand$1.call(LoadBalancerCommand.java:184) ~[ribbon-loadbalancer-2.2.0.jar:2.2.0]
    at com.netflix.loadbalancer.reactive.LoadBalancerCommand$1.call(LoadBalancerCommand.java:180) ~[ribbon-loadbalancer-2.2.0.jar:2.2.0]
    at rx.Observable.unsafeSubscribe(Observable.java:10211) ~[rxjava-1.1.10.jar:1.1.10]
    at rx.internal.operators.OnSubscribeConcatMap.call(OnSubscribeConcatMap.java:94) ~[rxjava-1.1.10.jar:1.1.10]
    at rx.internal.operators.OnSubscribeConcatMap.call(OnSubscribeConcatMap.java:42) ~[rxjava-1.1.10.jar:1.1.10]
    at rx.Observable.unsafeSubscribe(Observable.java:10211) ~[rxjava-1.1.10.jar:1.1.10]
    at rx.internal.operators.OperatorRetryWithPredicate$SourceSubscriber$1.call(OperatorRetryWithPredicate.java:127) ~[rxjava-1.1.10.jar:1.1.10]
    at rx.internal.schedulers.TrampolineScheduler$InnerCurrentThreadScheduler.enqueue(TrampolineScheduler.java:73) ~[rxjava-1.1.10.jar:1.1.10]
    at rx.internal.schedulers.TrampolineScheduler$InnerCurrentThreadScheduler.schedule(TrampolineScheduler.java:52) ~[rxjava-1.1.10.jar:1.1.10]
    at rx.internal.operators.OperatorRetryWithPredicate$SourceSubscriber.onNext(OperatorRetryWithPredicate.java:79) ~[rxjava-1.1.10.jar:1.1.10]
    at rx.internal.operators.OperatorRetryWithPredicate$SourceSubscriber.onNext(OperatorRetryWithPredicate.java:45) ~[rxjava-1.1.10.jar:1.1.10]
    at rx.internal.util.ScalarSynchronousObservable$WeakSingleProducer.request(ScalarSynchronousObservable.java:276) ~[rxjava-1.1.10.jar:1.1.10]
    at rx.Subscriber.setProducer(Subscriber.java:209) ~[rxjava-1.1.10.jar:1.1.10]
    at rx.internal.util.ScalarSynchronousObservable$JustOnSubscribe.call(ScalarSynchronousObservable.java:138) ~[rxjava-1.1.10.jar:1.1.10]
    at rx.internal.util.ScalarSynchronousObservable$JustOnSubscribe.call(ScalarSynchronousObservable.java:129) ~[rxjava-1.1.10.jar:1.1.10]
    at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:48) ~[rxjava-1.1.10.jar:1.1.10]
    at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:30) ~[rxjava-1.1.10.jar:1.1.10]
    at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:48) ~[rxjava-1.1.10.jar:1.1.10]
    at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:30) ~[rxjava-1.1.10.jar:1.1.10]
    at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:48) ~[rxjava-1.1.10.jar:1.1.10]
    at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:30) ~[rxjava-1.1.10.jar:1.1.10]
    at rx.Observable.subscribe(Observable.java:10307) ~[rxjava-1.1.10.jar:1.1.10]
    at rx.Observable.subscribe(Observable.java:10274) ~[rxjava-1.1.10.jar:1.1.10]
    at rx.observables.BlockingObservable.blockForSingle(BlockingObservable.java:445) ~[rxjava-1.1.10.jar:1.1.10]
    at rx.observables.BlockingObservable.single(BlockingObservable.java:342) ~[rxjava-1.1.10.jar:1.1.10]
    at com.netflix.client.AbstractLoadBalancerAwareClient.executeWithLoadBalancer(AbstractLoadBalancerAwareClient.java:102) ~[ribbon-loadbalancer-2.2.0.jar:2.2.0]
    at org.springframework.cloud.netflix.feign.ribbon.LoadBalancerFeignClient.execute(LoadBalancerFeignClient.java:63) ~[spring-cloud-netflix-core-1.2.4.BUILD-SNAPSHOT.jar:1.2.4.BUILD-SNAPSHOT]
    at feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:97) ~[feign-core-9.3.1.jar:na]
    at feign.SynchronousMethodHandler.invoke(SynchronousMethodHandler.java:76) ~[feign-core-9.3.1.jar:na]
    at feign.hystrix.HystrixInvocationHandler$1.run(HystrixInvocationHandler.java:108) ~[feign-hystrix-9.3.1.jar:na]
    at com.netflix.hystrix.HystrixCommand$2.call(HystrixCommand.java:301) ~[hystrix-core-1.5.6.jar:1.5.6]
    at com.netflix.hystrix.HystrixCommand$2.call(HystrixCommand.java:297) ~[hystrix-core-1.5.6.jar:1.5.6]
    at rx.internal.operators.OnSubscribeDefer.call(OnSubscribeDefer.java:46) ~[rxjava-1.1.10.jar:1.1.10]
    at rx.internal.operators.OnSubscribeDefer.call(OnSubscribeDefer.java:35) ~[rxjava-1.1.10.jar:1.1.10]
    at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:48) ~[rxjava-1.1.10.jar:1.1.10]
    at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:30) ~[rxjava-1.1.10.jar:1.1.10]
    at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:48) ~[rxjava-1.1.10.jar:1.1.10]
    at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:30) ~[rxjava-1.1.10.jar:1.1.10]
    at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:48) ~[rxjava-1.1.10.jar:1.1.10]
    at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:30) ~[rxjava-1.1.10.jar:1.1.10]
    at rx.Observable.unsafeSubscribe(Observable.java:10211) ~[rxjava-1.1.10.jar:1.1.10]
    at rx.internal.operators.OnSubscribeDefer.call(OnSubscribeDefer.java:51) ~[rxjava-1.1.10.jar:1.1.10]
    at rx.internal.operators.OnSubscribeDefer.call(OnSubscribeDefer.java:35) ~[rxjava-1.1.10.jar:1.1.10]
    at rx.Observable.unsafeSubscribe(Observable.java:10211) ~[rxjava-1.1.10.jar:1.1.10]
    at rx.internal.operators.OnSubscribeDoOnEach.call(OnSubscribeDoOnEach.java:41) ~[rxjava-1.1.10.jar:1.1.10]
    at rx.internal.operators.OnSubscribeDoOnEach.call(OnSubscribeDoOnEach.java:30) ~[rxjava-1.1.10.jar:1.1.10]
    at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:48) ~[rxjava-1.1.10.jar:1.1.10]
    at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:30) ~[rxjava-1.1.10.jar:1.1.10]
    at rx.Observable.unsafeSubscribe(Observable.java:10211) ~[rxjava-1.1.10.jar:1.1.10]
    at rx.internal.operators.OperatorSubscribeOn$1.call(OperatorSubscribeOn.java:94) ~[rxjava-1.1.10.jar:1.1.10]
    at com.netflix.hystrix.strategy.concurrency.HystrixContexSchedulerAction$1.call(HystrixContexSchedulerAction.java:56) ~[hystrix-core-1.5.6.jar:1.5.6]
    at com.netflix.hystrix.strategy.concurrency.HystrixContexSchedulerAction$1.call(HystrixContexSchedulerAction.java:47) ~[hystrix-core-1.5.6.jar:1.5.6]
    at com.netflix.hystrix.strategy.concurrency.HystrixContexSchedulerAction.call(HystrixContexSchedulerAction.java:69) ~[hystrix-core-1.5.6.jar:1.5.6]
    at rx.internal.schedulers.ScheduledAction.run(ScheduledAction.java:55) ~[rxjava-1.1.10.jar:1.1.10]
    at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) ~[na:1.8.0_51]
    at java.util.concurrent.FutureTask.run(FutureTask.java:266) ~[na:1.8.0_51]
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) ~[na:1.8.0_51]
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) ~[na:1.8.0_51]
    at java.lang.Thread.run(Thread.java:745) [na:1.8.0_51]

I tried different way. But couldn't figure out the solution. I couldn't find any information about this on Spring Cloud Netflix too. I couldn't find any information on discussion thread on StackOverflow too.

What I'm missing here? TIA.

Further Details:

application.yml for movie-api microservice is as below:

server:
  port: 8090
logging:
  config: classpath:log4j2.yml
spring:
  application:
    name: movie-api

Spring boot app on movie-api module:

@SpringBootApplication
public class MovieApiApplication {

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

And controller is as below:

@RestController
@Slf4j
public class MovieController {
    @Autowired
    private MovieRepository movieRepository;

    @RequestMapping("/movies/{id}")
    public ResponseEntity<Movie> getBook(@PathVariable("id") Long id){
        log.trace("getBook({})",id);
        Optional<Movie> movie = Optional.of(movieRepository.findOneById(id));
        if(movie.isPresent())
            return new ResponseEntity(movie.get(), HttpStatus.OK);
        return new ResponseEntity<Movie>(HttpStatus.NOT_FOUND);
    }
}

Answer:

After doing research, and with help of @Bloodysock, I found that I was missing registration of remote server in 'client-app' micro-service. The document is at Spring Cloud Netflix.

I used Ribbon without Eureka with configuration in application.yml in 'client-app' micro-service as:

movie-api:
  ribbon:
    listOfServers: http://localhost:8090

Question:

I'm building an application using microservices with the netflix stack and spring boot. One thing that bugs me is that I have no integration tests yet, where I can mock the surrounding services.

So, I have service A which is a eureka client with ribbon to resolve the eureka name to the URL of a registered service B during a call.

So ideally I want to start the application with the integrationtest annotations of spring boot, use wiremock to simulate the service B and then call the method of service A, this should call my mocked service B using the symbolic name of the service.

Did anyone already solve this? I have searched for blog entries etc. of people doing this already, but couldn't find any...

I know of the SO article Mock an Eureka Feign Client for Unittesting but as far as I can see this just prevents the discovery client from complaining.


Answer:

The following code (taken from https://github.com/Netflix/eureka/blob/a7a8d278e6399bbff5faa49b9fcbcd7ea9e854f4/eureka-core/src/test/java/com/netflix/eureka/mock/MockRemoteEurekaServer.java) may be helpful to you;

public class MockRemoteEurekaServer extends ExternalResource {

public static final String EUREKA_API_BASE_PATH = "/eureka/v2/";

private final Map<String, Application> applicationMap;
private final Map<String, Application> applicationDeltaMap;
private final Server server;
private boolean sentDelta;
private int port;
private volatile boolean simulateNotReady;

public MockRemoteEurekaServer(int port, Map<String, Application> applicationMap,
                              Map<String, Application> applicationDeltaMap) {
    this.applicationMap = applicationMap;
    this.applicationDeltaMap = applicationDeltaMap;
    ServletHandler handler = new AppsResourceHandler();
    EurekaServerConfig serverConfig = new DefaultEurekaServerConfig();
    EurekaServerContext serverContext = mock(EurekaServerContext.class);
    when(serverContext.getServerConfig()).thenReturn(serverConfig);

    handler.addFilterWithMapping(ServerRequestAuthFilter.class, "/*", 1).setFilter(new ServerRequestAuthFilter(serverContext));
    handler.addFilterWithMapping(RateLimitingFilter.class, "/*", 1).setFilter(new RateLimitingFilter(serverContext));
    server = new Server(port);
    server.addHandler(handler);
    System.out.println(String.format(
            "Created eureka server mock with applications map %s and applications delta map %s",
            stringifyAppMap(applicationMap), stringifyAppMap(applicationDeltaMap)));
}

@Override
protected void before() throws Throwable {
    start();
}

@Override
protected void after() {
    try {
        stop();
    } catch (Exception e) {
        Assert.fail(e.getMessage());
    }
}

public void start() throws Exception {
    server.start();
    port = server.getConnectors()[0].getLocalPort();
}

public void stop() throws Exception {
    server.stop();
}

public boolean isSentDelta() {
    return sentDelta;
}

public int getPort() {
    return port;
}

public void simulateNotReady(boolean simulateNotReady) {
    this.simulateNotReady = simulateNotReady;
}

private static String stringifyAppMap(Map<String, Application> applicationMap) {
    StringBuilder builder = new StringBuilder();
    for (Map.Entry<String, Application> entry : applicationMap.entrySet()) {
        String entryAsString = String.format("{ name : %s , instance count: %d }", entry.getKey(),
                entry.getValue().getInstances().size());
        builder.append(entryAsString);
    }
    return builder.toString();
}

private class AppsResourceHandler extends ServletHandler {

    @Override
    public void handle(String target, HttpServletRequest request, HttpServletResponse response, int dispatch)
            throws IOException, ServletException {

        if (simulateNotReady) {
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            return;
        }
        String authName = request.getHeader(AbstractEurekaIdentity.AUTH_NAME_HEADER_KEY);
        String authVersion = request.getHeader(AbstractEurekaIdentity.AUTH_VERSION_HEADER_KEY);
        String authId = request.getHeader(AbstractEurekaIdentity.AUTH_ID_HEADER_KEY);

        Assert.assertNotNull(authName);
        Assert.assertNotNull(authVersion);
        Assert.assertNotNull(authId);

        Assert.assertTrue(!authName.equals(ServerRequestAuthFilter.UNKNOWN));
        Assert.assertTrue(!authVersion.equals(ServerRequestAuthFilter.UNKNOWN));
        Assert.assertTrue(!authId.equals(ServerRequestAuthFilter.UNKNOWN));

        for (FilterHolder filterHolder : this.getFilters()) {
            filterHolder.getFilter().doFilter(request, response, new FilterChain() {
                @Override
                public void doFilter(ServletRequest request, ServletResponse response)
                        throws IOException, ServletException {
                    // do nothing;
                }
            });
        }

        String pathInfo = request.getPathInfo();
        System.out.println(
                "Eureka resource mock, received request on path: " + pathInfo + ". HTTP method: |" + request
                        .getMethod() + '|');
        boolean handled = false;
        if (null != pathInfo && pathInfo.startsWith("")) {
            pathInfo = pathInfo.substring(EUREKA_API_BASE_PATH.length());
            if (pathInfo.startsWith("apps/delta")) {
                Applications apps = new Applications();
                for (Application application : applicationDeltaMap.values()) {
                    apps.addApplication(application);
                }
                apps.setAppsHashCode(apps.getReconcileHashCode());
                sendOkResponseWithContent((Request) request, response, toJson(apps));
                handled = true;
                sentDelta = true;
            } else if (pathInfo.startsWith("apps")) {
                Applications apps = new Applications();
                for (Application application : applicationMap.values()) {
                    apps.addApplication(application);
                }
                apps.setAppsHashCode(apps.getReconcileHashCode());
                sendOkResponseWithContent((Request) request, response, toJson(apps));
                handled = true;
            }
        }

        if (!handled) {
            response.sendError(HttpServletResponse.SC_NOT_FOUND,
                    "Request path: " + pathInfo + " not supported by eureka resource mock.");
        }
    }

    private void sendOkResponseWithContent(Request request, HttpServletResponse response, String content)
            throws IOException {
        response.setContentType("application/json; charset=UTF-8");
        response.setStatus(HttpServletResponse.SC_OK);
        response.getOutputStream().write(content.getBytes("UTF-8"));
        response.getOutputStream().flush();
        request.setHandled(true);
        System.out.println("Eureka resource mock, sent response for request path: " + request.getPathInfo() +
                " with content" + content);
    }
}

private String toJson(Applications apps) throws IOException {
    return new EurekaJsonJacksonCodec().getObjectMapper(Applications.class).writeValueAsString(apps);
}

}

Question:

Spring Cloud Config Server accepts multiple profile and returns the properties for all the profiles when I access the /env endpoint of the application. The response lists the properties specific to each profile. If same property is present in 2 different property files , the one that is defined last takes precedence. Is there a way to get the final list of property key and values that will be used by the application?


Answer:

For Cloud Config Client Application

I've tried different ways and found the following (accidentally):

GET /env/.* returns full list of configuration properties

For Cloud Config Server Application

It turns out this is already implemented, but not documented well. All you need is to request json, yml or properties according to the patterns:

/{application}-{profile}.{ext}
/{label}/{application}-{profile}.{ext}

Question:

I have a custom gateway filter MYGatewayFilter.java file now i want to use this gateway filter with my route written in application.yml

 spring:
  cloud:
   gateway:
    routes:
      - id: login2_route
      uri: http://127.0.0.1:8083/login
      predicates:
       - Path: /login/
      filters:

How do i define filters for above route

Custom Filter MyGatewayFilter.java

public class MyGatewayFilter implements GatewayFilter {
    @Override
  public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
      ServerHttpRequest request;
      if(request.getHeaders().get("x-mydata")!=null){
         request= exchange.getRequest().mutate().header("my-new-header",request.getHeaders().get("x-mydata").get(0)).build();
      }

      return chain.filter(exchange.mutate().request(request).build());
  }
}       

Answer:

Instead of implementing GatewayFilter you should implement GatewayFilterFactory

and make it a Component:

@Component
public class MyGatewayFilter implements GatewayFilterFactory {

Then you can refer to it by the bean name in your route.

filters:
- MyGatewayFilter

The documentation on this isn't very good at the moment. I was only able to figure this out by looking at the source code for spring-cloud-gateway on github