How to implement "load balancer" using spring boot? - java

Depends on request body content I need to redirect http requests to URL_1 or URL_2.
I started controller implementation:
#RestController
public class RouteController {
#Autowired
private RestTemplate restTemplate;
#RequestMapping(value = "/**")
public HttpServletResponse route(HttpServletRequest request) {
String body = IOUtils.toString(request.getReader());
if(isFirstServer(body)) {
//send request to URL_1 and get response
} else {
//send request to URL_2 and get response
}
}
}
Request might be GET or POST ot PUT or PATCH etc.
Could you help me to write that code?

I've asked a somehow similar question a while ago. Plea see Server side redirect for REST call for more context.
The best way (to my current understanding) you could achieve this is by manually invoking the desired endpoints from your initial endpoint.
#RestController
public class RouteController {
#Value("${firstUrl}")
private String firstUrl;
#Value("${secondUrl}")
private String secondUrl;
#Autowired
private RestTemplate restTemplate;
#RequestMapping(value = "/**")
public void route(HttpServletRequest request) {
String body = IOUtils.toString(request.getReader());
if(isFirstServer(body)) {
restTemplate.exchange(firstUrl,
getHttpMethod(request),
getHttpEntity(request),
getResponseClass(request),
getParams(params));
} else {
restTemplate.exchange(secondUrl,
getHttpMethod(request),
getHttpEntity(request),
getResponseClass(request),
getParams(params))
}
}
}
Example implementation for getHttpMethod:
public HttpMethod getHttpMethod(HttpServletRequest request) {
return HttpMethod.valueOf(request.getMethod());
}
Similar implementations for getHttpEntity, getResponseClass and getParams. They are used for converting the data from the HttpServletRequest request to the types required by the exchange method.
There seem to be a lot of better ways of doing this for a Spring MVC app, but I guess that it does not apply to your context.
Another way you could achieve this would be defining your own REST client and adding the routing logic there.

Related

PageNotFound - Request method 'GET' not supported

I am getting this error while calling an API from postman, after I hosted my spring app in VM. Locally it works. But Get methods in my VMs are working.
[http-nio-8081-exec-4] PageNotFound - Request method 'GET' not supported
My controller method looks like this:
#RestController
#RequestMapping("/orders/")
public class OrdersController {}
#PostMapping(value = "create", produces = "text/plain")
private String createOrder(#RequestBody POCreationRequest request) throws ParseException {
The API request running forever and dont get any response. I found the exception in my log. Any idea on this issue?
You created two urls there:
url/orders/ -> accepts get/post/etc... (though its not implemented)
url/orders/create -> accepts post
#RestController
#RequestMapping("/orders")
public class OrdersController {
#PostMapping(value = "create", produces = "text/plain")
private String createOrder(#RequestBody POCreationRequest request) throws ParseException {
System.out.println(request)}
}
You can try the above code.
You are trying to make a GET request on an only POST endpoint, thus then not loading the page. Your endpoint should be of type GET. You can also have the same endpoint for GET and POST requests as follows:
#RestController
#RequestMapping("/orders/")
public class OrdersController {}
#PostMapping(value = "create", produces = "text/plain")
private String createOrder(#RequestBody POCreationRequest request) throws ParseException {
//Parse post requests
}
#GetMapping(value= "create")
private String servePage() {
return create; //create is the name of the html view.
}
Now when going to localhost:8080/orders/create it should serve the view.
You can also make the GET mapping return a JSON object by:
#GetMapping(value= "create")
private String serveJSON() {
return "{\"hello\": \"world\"}"; //The string is treated as JSON and not as a view.
}

How to log RequestBody in async spring controller?

I added an async endpoint to a existing spring-mvc application:
#RestController
public class MyController {
#PostMapping("/")
public Mono<String> post(Object body) {
return Mono.just("test");
//webClient.retrieve().bodyToMono(String.class);
}
}
I want to create a global interceptor/filter that will log the request body payload. But how can I get access to it?
I tried adding a HandlerInterceptorAdapter, but the payload is always empty:
static class LoggingInterceptor extends HandlerInterceptorAdapter {
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
ContentCachingRequestWrapper wrapper = new ContentCachingRequestWrapper(request);
byte[] buf = wrapper.getContentAsByteArray();
System.out.println(buf);
System.out.println(buf.length);
return true;
}
}
Maybe the payload is not yet present in the request, or has already been read. So how can I access the body in this async case?
Unfortunately in Webflux you cannot use HandlerInterceptorAdapter because it came from web mvc module and works only with the servlets.
I found a good article with solutions.
P.S. You must to remove spring-mvc dependencies if going to use reactive endpoins.

Spring - Rest controller handles json input as null string

While playing around with the facebook messenger api I created a simple REST controller
#RestController
public class ChatController
{
private static final Logger LOG = LoggerFactory.getLogger(ChatController.class);
#RequestMapping(value="/webhook", method=RequestMethod.POST, consumes="application/json")
public String onWebhookEvent(String event)
{
LOG.info("Received event {}",event);
return "test";
}
}
However, when I POST the following json to the the /webhook endpoint the event input is logged as null ("Received event null")
{"object":
"page",
"entry":[
{
"id":43674671559,
"time":1460620433256,
"messaging":[
{"sender":{"id":123456789},
"recipient":{"id":987654321},
"timestamp":1460620433123,
"message":{"mid":"mid.1460620432888:f8e3412003d2d1cd93","seq":12604,"text":"Testing Chat Bot .."}
}
]
}
]
}
Why is that and how can I fix that? Since json is a serialization mechanism I assumed it will be presented as string to the onWebhookEvent method.
Thanks for the help
If you want a request's body to be tied up to a parameter, use #RequestBody.
By the way, return a ResponseEntity object, as it is a wrapper to whatever you want to return, and you can specify additional information (for example, headers of the response)

How to handle JWT Authentication with Spring when implementing a CQRS pattern?

Using the latest Spring Cloud and Spring Boot, I've got a micro services layout with a Zuul gateway. At the moment when a user sends a get request their JWT token gets added to the request and that goes off to the microservice where they're authenticated and things go as usual. This all works perfectly.
Where I'm a little stuck is when handling POST/PATCH/DELETE requests. These don't go directly to the microservice they're destined for, but instead go into a messaging queue. The queue contains a simple POJO with a task and information about the task to perform, along with the users JWT.
When the receiving microservice picks up the message from the queue and starts processing it, the user isn't technically logged into the microservice like they are with a GET request. This makes it hard to do things that require knowing who the user is. Sure each time I need to know who the person is I can look them up, but that seems clunky.
I've thought about creating a REST controller for the POST/PATCH/DELETE commands and having the queue listener just call itself from these, adding the token from the task. This would effectively be the same as a GET request as far as Spring security would care.
Is this the proper pattern? Or is there a simple programmatically way to log a user in with a JWT? I've seen a few examples using Username/Passwords but not sure how to transcribe that to using a JWT.
Thank you Andy Brown for the confirmation I wasn't completely nuts doing it this way. For anyone interested it's a very simple solution that looks like this:
The queue service is just listening for events (In this case from AWS SQS) and as soon as an event comes through it gets fired off to a command controller for processing.
#Service
#EnableSqs
public class QueueListener {
private final static String SERVICE = "http://instance-service/instances";
#Autowired
private JsonTransformService jsonTransformService;
#Autowired
private RestTemplate restTemplate;
#MessageMapping("${queues.instanceEvents}")
public void instanceCommandHandler(String payload) {
// Transform the payload to the object so we can get the preset JWT
Instance instance = jsonTransformService.read(Instance.class, payload);
// Load the JWT into the internal request header, without this a 403 is thrown
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + instance.getUserToken());
HttpEntity<String> instanceEntity = new HttpEntity<>(payload, headers);
// Decide and set where to fire the request to
String endpoint;
switch (instance.getSwordfishCommand()) {
case "start": {
endpoint = "/start";
break;
}
case "stop": {
endpoint = "/stop";
break;
}
case "create": {
endpoint = "/create";
break;
}
case "reboot": {
endpoint = "/reboot";
break;
}
case "terminate": {
endpoint = "/terminate";
break;
}
default: {
endpoint = "/error";
}
}
// Fire the initial string payload through to the correct controller endpoint
restTemplate.exchange(SERVICE + endpoint, HttpMethod.POST, instanceEntity, String.class);
}
}
And a very simple REST controller which actions the tasks
#RestController
#RequestMapping("/instances")
public class InstanceCommandController {
#Autowired
private EC2Create ec2Create;
#Autowired
private EC2Start ec2Start;
#Autowired
private EC2Stop ec2Stop;
#Autowired
private EC2Reboot ec2Reboot;
#Autowired
private EC2Terminate ec2Terminate;
#Autowired
private JsonTransformService jsonTransformService;
#PostMapping("/create")
public void create(#RequestBody String payload) {
ec2Create.process(jsonTransformService.read(Instance.class, payload));
}
#PostMapping("/start")
public void start(#RequestBody String payload) {
ec2Start.process(jsonTransformService.read(Instance.class, payload));
}
#PostMapping("/stop")
public void stop(#RequestBody String payload) {
ec2Stop.process(jsonTransformService.read(Instance.class, payload));
}
#PostMapping("/reboot")
public void reboot(#RequestBody String payload) {
ec2Reboot.process(jsonTransformService.read(Instance.class, payload));
}
#PostMapping("/terminate")
public void terminate(#RequestBody String payload) {
ec2Terminate.process(jsonTransformService.read(Instance.class, payload));
}
}
This follows the CQRS pattern very well while still authenticating the user on each call. For me this is very useful as I have a AmazonEC2Async client which makes use of the users own access and secret token in each request.
Cheers for the help!

Spring Cloud Feign Interceptor

I have created a ClientHttpRequestInterceptor that I use to intercept all outgoing RestTemplate requests and responses. I would like to add the interceptor to all outgoing Feign requests/responses. Is there a way to do this?
I know that there is a feign.RequestInterceptor but with this I can only intercept the request and not the response.
There is a class FeignConfiguration that I found in Github that has the ability to add interceptors but I don't know in which maven dependency version it is.
A practical example of how to intercept the response in a Spring Cloud OpenFeign.
Create a custom Client by extending Client.Default as shown below:
public class CustomFeignClient extends Client.Default {
public CustomFeignClient(SSLSocketFactory sslContextFactory, HostnameVerifier hostnameVerifier) {
super(sslContextFactory, hostnameVerifier);
}
#Override
public Response execute(Request request, Request.Options options) throws IOException {
Response response = super.execute(request, options);
InputStream bodyStream = response.body().asInputStream();
String responseBody = StreamUtils.copyToString(bodyStream, StandardCharsets.UTF_8);
//TODO do whatever you want with the responseBody - parse and modify it
return response.toBuilder().body(responseBody, StandardCharsets.UTF_8).build();
}
}
Then use the custom Client in a configuration class:
public class FeignClientConfig {
public FeignClientConfig() { }
#Bean
public Client client() {
return new CustomFeignClient(null, null);
}
}
Finally, use the configuration class in a FeignClient:
#FeignClient(name = "api-client", url = "${api.base-url}", configuration = FeignClientConfig.class)
public interface ApiClient {
}
Good luck
If you want to use feign from spring cloud, use org.springframework.cloud:spring-cloud-starter-feign as your dependency coordinates. Currently the only way to modify the response is to implement your own feign.Client.

Categories