Learning Spring-Cloud – Writing a microservice
Continuing my Spring-Cloud learning journey, earlier I had covered how to write the infrastructure components of a typical Spring-Cloud and Netflix OSS based micro-services environment – in this specific instance two critical components, Eureka to register and discover services and Spring Cloud Configuration to maintain a centralized repository of configuration for a service. Here I will be showing how I developed two dummy micro-services, one a simple “pong” service and a “ping” service which uses the “pong” service.
Sample-Pong microservice
The endpoint handling the “ping” requests is a typical Spring MVC based endpoint:
@RestController public class PongController { @Value("${reply.message}") private String message; @RequestMapping(value = "/message", method = RequestMethod.POST) public Resource<MessageAcknowledgement> pongMessage(@RequestBody Message input) { return new Resource<>( new MessageAcknowledgement(input.getId(), input.getPayload(), message)); } }
It gets a message and responds with an acknowledgement. Here the service utilizes the Configuration server in sourcing the “reply.message” property. So how does the “pong” service find the configuration server, there are potentially two ways – directly by specifying the location of the configuration server, or by finding the Configuration server via Eureka. I am used to an approach where Eureka is considered a source of truth, so in this spirit I am using Eureka to find the Configuration server. Spring Cloud makes this entire flow very simple, all it requires is a “bootstrap.yml” property file with entries along these lines:
--- spring: application: name: sample-pong cloud: config: discovery: enabled: true serviceId: SAMPLE-CONFIG eureka: instance: nonSecurePort: ${server.port:8082} client: serviceUrl: defaultZone: http://${eureka.host:localhost}:${eureka.port:8761}/eureka/
The location of Eureka is specified through the “eureka.client.serviceUrl” property and the “spring.cloud.config.discovery.enabled” is set to “true” to specify that the configuration server is discovered via the specified Eureka server.
Just a note, this means that the Eureka and the Configuration server have to be completely up before trying to bring up the actual services, they are the pre-requisites and the underlying assumption is that the Infrastructure components are available at the application boot time.
The Configuration server has the properties for the “sample-pong” service, this can be validated by using the Config-servers endpoint – http://localhost:8888/sample-pong/default, 8888 is the port where I had specified for the server endpoint, and should respond with a content along these lines:
"name": "sample-pong", "profiles": [ "default" ], "label": "master", "propertySources": [ { "name": "classpath:/config/sample-pong.yml", "source": { "reply.message": "Pong" } } ] }
As can be seen the “reply.message” property from this central configuration server will be used by the pong service as the acknowledgement message
Now to set up this endpoint as a service, all that is required is a Spring-boot based entry point along these lines:
@SpringBootApplication @EnableDiscoveryClient public class PongApplication { public static void main(String[] args) { SpringApplication.run(PongApplication.class, args); } }
and that completes the code for the “pong” service.
Sample-ping micro-service
So now onto a consumer of the “pong” micro-service, very imaginatively named the “ping” micro-service. Spring-Cloud and Netflix OSS offer a lot of options to invoke endpoints on Eureka registered services, to summarize the options that I had:
- Use raw Eureka DiscoveryClient to find the instances hosting a service and make calls using Spring’s RestTemplate.
- Use Ribbon, a client side load balancing solution which can use Eureka to find service instances
- Use Feign, which provides a declarative way to invoke a service call. It internally uses Ribbon.
I went with Feign. All that is required is an interface which shows the contract to invoke the service:
package org.bk.consumer.feign; import org.bk.consumer.domain.Message; import org.bk.consumer.domain.MessageAcknowledgement; import org.springframework.cloud.netflix.feign.FeignClient; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; @FeignClient("samplepong") public interface PongClient { @RequestMapping(method = RequestMethod.POST, value = "/message", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) @ResponseBody MessageAcknowledgement sendMessage(@RequestBody Message message); }
The annotation @FeignClient(“samplepong”) internally points to a Ribbon “named” client called “samplepong”. This means that there has to be an entry in the property files for this named client, in my case I have these entries in my application.yml file:
samplepong: ribbon: DeploymentContextBasedVipAddresses: sample-pong NIWSServerListClassName: com.netflix.niws.loadbalancer.DiscoveryEnabledNIWSServerList ReadTimeout: 5000 MaxAutoRetries: 2
The most important entry here is the “samplepong.ribbon.DeploymentContextBasedVipAddresses” which points to the “pong” services Eureka registration address using which the service instance will be discovered by Ribbon.
The rest of the application is a routine Spring Boot application. I have exposed this service call behind Hystrix which guards against service call failures and essentially wraps around this FeignClient:
package org.bk.consumer.service; import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand; import org.bk.consumer.domain.Message; import org.bk.consumer.domain.MessageAcknowledgement; import org.bk.consumer.feign.PongClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; @Service("hystrixPongClient") public class HystrixWrappedPongClient implements PongClient { @Autowired @Qualifier("pongClient") private PongClient feignPongClient; @Override @HystrixCommand(fallbackMethod = "fallBackCall") public MessageAcknowledgement sendMessage(Message message) { return this.feignPongClient.sendMessage(message); } public MessageAcknowledgement fallBackCall(Message message) { MessageAcknowledgement fallback = new MessageAcknowledgement(message.getId(), message.getPayload(), "FAILED SERVICE CALL! - FALLING BACK"); return fallback; } }
“Boot”ing up
I have dockerized my entire set-up, so the simplest way to start up the set of applications is to first build the docker images for all of the artifacts this way:
mvn clean package docker:build -DskipTests
and bring all of them up using the following command, the assumption being that both docker and docker-compose are available locally:
docker-compose up
Assuming everything comes up cleanly, Eureka should show all the registered services, at http://dockerhost:8761 url –
The UI of the ping application should be available at http://dockerhost:8080 url –
Additionally a Hystrix dashboard should be available to monitor the requests to the “pong” app at this url http://dockerhost:8989/hystrix/monitor?stream=http%3A%2F%2Fsampleping%3A8080%2Fhystrix.stream:
References
- The code is available at my github location – https://github.com/bijukunjummen/spring-cloud-ping-pong-sample
- Most of the code is heavily borrowed from the spring-cloud-samples repository – https://github.com/spring-cloud-samples
Reference: | Learning Spring-Cloud – Writing a microservice from our JCG partner Biju Kunjummen at the all and sundry blog. |
The gateway project included on github is starting up, but it does not route requests to services.
I did not see any mention about the gateway in the text, but should it be working or is more configuration needed?