Setting Up Reservation Client (Edge Service)
Introduction
We are now at our last service to build which is our edge service. Edge Service pattern is widely used in microservices architecture to hide multiple backend services behind one gateway/ front-door. It is similar to the Facade design pattern, however, an edge service not only routes requests to the correct backend services, it usually serves as a point to insert common cross-cutting features such as security (CORS prevention), authentication, and fault tolerance. You will usually also put your React/ Angular SPA in this edge service. Moreover, an edge service is also a logical place to insert API translation or protocol translation requirements.
Zuul is Netflix’s micro proxy solution we will use. It will sit as an intermediary between clients (e.g. smart phones, HTML5 applications) and our service. Our example will only show a client for HTML5 browser.
Setting up Reservation Client’s Configuration
In the existing config’s git:
vim reservation-client.properties
### Add the below to reservation-service.properties
server.port=${PORT:9999}
# define the destination to which the output MessageChannel should be bound
spring.cloud.stream.bindings.output.destination=reservations
# define oauth URL
security.oauth2.resource.userInfoUri=http://localhost:9191/uaa/user
# To avoid sending zipkin spans/ traces via kafka
spring.zipkin.sender.type=web
## End of reservation-client.properties
git add reservation-client.properties
git commit -m "Add reservation-client.properties"
Setting up Reservation Client
-
Go to https://start.spring.io/
-
Type
reservation-client
as the “Artifact” name, addWeb
,Lombok
,Feign
,Zuul
,Hystrix
,Stream Kafka
,Eureka Discovery
,Config Client
,Cloud OAuth2
,HATEOAS
andZipkin Client
dependencies, and “Generate Project”: -
Open the project in your favorite Java IDE (mine is IntelliJ)
-
Since we’ll have to have a domain model in the client side as well, again, create a simple entity
Reservation
withid
andreservationName
and furnish it with the below Lombok annotations:package com.example.reservationclient; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @AllArgsConstructor @NoArgsConstructor @Data public class Reservation { private Long id; private String reservationName; }
-
Define
ReservationReader
interface which is aFeignClient
:package com.example.reservationclient; import org.springframework.cloud.netflix.feign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import java.util.Collection; @FeignClient("reservation-service") interface ReservationReader { @GetMapping("/reservations") Collection<Reservation> read(); }
Feign is yet another Java http client developed by Netflix. Its primary selling point is its declarative style which saves a lot of overheads.
-
If we just want a dumb proxy,
@EnableZuulProxy
will suffice. However, in this scenario, our API gateway usually needs to do some “work”. Create a class calledReservationApiGatewayRestController
:package com.example.reservationclient; import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.stream.messaging.Source; import org.springframework.core.ParameterizedTypeReference; import org.springframework.hateoas.Resources; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.support.MessageBuilder; 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.RestController; import org.springframework.web.client.RestTemplate; import java.util.Collection; import java.util.Collections; import java.util.stream.Collectors; @RestController @RequestMapping("/reservations") public class ReservationApiGatewayRestController { private ReservationReader reservationReader; private MessageChannel output; @Autowired public ReservationApiGatewayRestController(ReservationReader reservationReader, Source source) { this.reservationReader = reservationReader; this.output = source.output(); } public Collection<String> fallback() { System.out.println("fallback hit"); return Collections.EMPTY_LIST; } @RequestMapping(method = RequestMethod.GET, value="names") @HystrixCommand(fallbackMethod = "fallback") public Collection<String> getReservationNames() { return this.reservationReader.read() .stream() .map(Reservation::getReservationName) .collect(Collectors.toList()); } @RequestMapping(method = RequestMethod.POST) public void write(@RequestBody Reservation r) { Message<String> msg = MessageBuilder.withPayload(r.getReservationName()).build(); this.output.send(msg); } }
- There is a lot happening above. First we declare a RESTful endpoint “reservations”.
- It has the usual Spring constructor injection setup. We need 2 beans: 1)
RestTemplate
to relay calls to our backend reservation service AND 2)MessageChannel
obtained viaSource
sink to push write messages to our backend reservation-service as well. - We define a fallback method for Hystrix circuit breaker, we name the method “
fallback
”. It just returns an empty List. - Next, we define a GET endpoint called “
names
” and inside, we use our feign clientreservationReader
to get the reservations from our backend service and massage the data to only return the names. Note that it’s annotated withHystrixCommand
withfallbackMethod
calledfallback
(as defined in 3). In case there’s any exception (e.g. our backend service is down), this endpoint will run the fallback method instead of reporting back the ugly exception message. - Finally, we have the POST endpoint for write which just send a message to the
output
channel.
-
Create a class called
RateLimiterFilter
below:package com.example.reservationclient; import com.google.common.util.concurrent.RateLimiter; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants; import org.springframework.core.Ordered; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletResponse; @Component class RateLimitingZuulFilter extends ZuulFilter { private RateLimiter rateLimiter = RateLimiter.create(.1); @Override public String filterType() { return FilterConstants.PRE_TYPE; } @Override public int filterOrder() { return Ordered.HIGHEST_PRECEDENCE; } @Override public boolean shouldFilter() { return true; } @Override public Object run() { RequestContext currentContext = RequestContext.getCurrentContext(); HttpServletResponse response = currentContext.getResponse(); if (!rateLimiter.tryAcquire()) { currentContext.setSendZuulResponse(false); response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value()); } return null; } }
NOTE:
- To avoid our API gateways from being bombarded with requests, we usually set up what’s called rate limiters. Basically, requests happening more frequently than a specified range will be bounced back.
- Spring will register all filters as long as it’s annotated
@Component
and extendsZuulFilter
- We use Guava’s rate limiter with value
0.1
. It means 1 request every 10 seconds. ZuulFilter
s only filters Zuul’s proxied services (e.g. not our defined RestControllerReservationApiGatewayController
)
-
And finally, in
ReservationClientApplication
:package com.example.reservationclient; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.cloud.netflix.feign.EnableFeignClients; import org.springframework.cloud.netflix.zuul.EnableZuulProxy; import org.springframework.cloud.stream.annotation.EnableBinding; import org.springframework.context.annotation.Bean; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; import org.springframework.web.client.RestTemplate; import org.springframework.cloud.stream.messaging.Source; @SpringBootApplication @EnableResourceServer @EnableBinding(Source.class) @EnableCircuitBreaker @EnableFeignClients @EnableDiscoveryClient @EnableZuulProxy public class ReservationClientApplication { @Bean @LoadBalanced RestTemplate restTemplate() { return new RestTemplate(); } public static void main(String[] args) { SpringApplication.run(ReservationClientApplication.class, args); } }
Let me have a shot as explaining all the annotations above:
@EnableResourceServer
is to activate OAuth2 security@EnableBinding
is to set up our channel with default nameoutput
@EnableCircuitBreaker
is to tell hystrix to activate its circuit breaker@EnableFeignClients
&@LoadBalanced
are to use Feign as our web service client (it’s integrated with Eureka and can provide load-balancing)@EnableDiscoveryClient
is again, to register our service to Eureka@EnableZuulProxy
is our main annotation to tell Zuul to proxy all services in Eureka in thisreservation-client
-
in
src/main/resources
, create a new file calledbootstrap.properties
, add the below:# tell config service what is the name of the service and corresponding config to retrieve spring.application.name=reservation-client # tell the location the location of config server spring.cloud.config.uri=http://localhost:8888
-
Make sure you run all the other services in the previous posts (including Kafka). When they are all booted up, run
ReservationClientApplication
and finally our edge service is started! -
Let’s first test our vanilla Zuul proxied services at http://localhost:9999/reservation-service/reservations. You should get an authorized error message, which means our spring security is working correctly:
<oauth> <error_description> Full authentication is required to access this resource </error_description> <error>unauthorized</error> </oauth>
-
Open your favorite Terminal/ Git Bash as we’ll need
curl
’s help for now onwards to test our services. -
Fire the below curl command to get an oauth token:
curl -X POST -vu android:secret http://localhost:9191/uaa/oauth/token -H "Accept: application/json" -d "password=spring&username=tomy&grant_type=password&scope=openid&client_secret=secret&client_id=android"
-
Now use your retrieved token:
curl http://localhost:9999/reservation-service/reservations -H "Authorization: Bearer 02d0ce25-c667-4571-a31c-91b14bc79fd9"
NOTE: Change
02d0ce25-c667-4571-a31c-91b14bc79fd9
with our own generated token from 13.you should get the below JSON results:
[{"id":1,"reservationName":"TOMY JAYA"},{"id":2,"reservationName":"JOHN DOE"}]
-
Now let’s see if our RateLimiter is working, use the same curl command within 10 seconds of the last request and you should not see anything returned.
-
Next, let’s test our API Gateway:
curl http://localhost:9999/reservations/names -H "Authorization: Bearer 02d0ce25-c667-4571-a31c-91b14bc79fd9"
you should get the below JSON results:
["TOMY JAYA","JOHN DOE"]
-
So far so good, let’s digress and see our Eureka dashboard at http://localhost:8761/. You should see our services properly registered:
-
Let’s test if our reservation-client breaks gracefully as we’ve set up a circuit breaker. You can kill our reservation-service instance then:
curl http://localhost:9999/reservations/names -H "Authorization: Bearer 02d0ce25-c667-4571-a31c-91b14bc79fd9"
you should get an empty array
[]
. However, if you hit the below:curl http://localhost:9999/reservation-service/reservations -H "Authorization: Bearer 02d0ce25-c667-4571-a31c-91b14bc79fd9"
you should see something like the below:
{"timestamp":1515170276976,"status":404,"error":"Not Found","message":"No message available","path":"/reservation-service/reservations"}
Now we understand the importance of guarding our clients using circuit breakers.
-
Okay, how about writes? Let’s try to add a new reservation:
curl -X POST -d '{"reservationName": "VALERIE"}' -H "Content-Type: application/json" -H "Authorization: Bearer 02d0ce25-c667-4571-a31c-91b14bc79fd9" http://localhost:9999/reservations
-
I’m aware that
reservation-service
is down. But no problem, we’ve designed for fail-safe writes. Let’s boot up ourreservation-service
now by runningReservationServiceApplication
. -
Wait for a while since Hystrix needs time to flip back the circuit breaker. When we query again:
curl http://localhost:9999/reservations/names -H "Authorization: Bearer 02d0ce25-c667-4571-a31c-91b14bc79fd9"
we should get the newly added reservation:
["TOMY JAYA","JOHN DOE","VALERIE"]
-
Open our hystrix dashboard at http://localhost:8010/hystrix.html. Put in http://localhost:9999/hystrix.stream in the textbox and click “Monitor”. Now, you should be able to see requests going to our circuit breakers. Play around by killing reservation-service and bringing it up again to see how the graph looks like.
-
Finally, let’s open our zipkin dashboard to see if we can trace our requests
select “reservation-client” and click “Find Traces”, and click on the first result:
we can observe how our request time spent between the
reservation-client
andreservation-service
.
Conclusion & Congratulations!
That’s it! Hope you learnt a thing or two from this series. If you have any questions, feel free to ask ‘em in the Disqus forum below.