RestTemplate or discoveryClient - which one to use in Spring Cloud application? - spring-boot

I am borrowing the below the code from Spring blog here.
#SpringBootApplication
#EnableEurekaClient
#EnableFeignClients
public class Application {
public static void main(String[] args) {
new SpringApplicationBuilder(Application.class)
.web(false)
.run(args);
}
}
#Component
class DiscoveryClientExample implements CommandLineRunner {
#Autowired
private DiscoveryClient discoveryClient;
#Override
public void run(String... strings) throws Exception {
discoveryClient.getInstances("photo-service").forEach((ServiceInstance s) -> {
System.out.println(ToStringBuilder.reflectionToString(s));
});
discoveryClient.getInstances("bookmark-service").forEach((ServiceInstance s) -> {
System.out.println(ToStringBuilder.reflectionToString(s));
});
}
}
#Component
class RestTemplateExample implements CommandLineRunner {
#Autowired
private RestTemplate restTemplate;
#Override
public void run(String... strings) throws Exception {
// use the "smart" Eureka-aware RestTemplate
ResponseEntity<List<Bookmark>> exchange =
this.restTemplate.exchange(
"http://bookmark-service/{userId}/bookmarks",
HttpMethod.GET,
null,
new ParameterizedTypeReference<List<Bookmark>>() {
},
(Object) "mstine");
exchange.getBody().forEach(System.out::println);
}
}
There are two options of consuming microservice endpoints from other microservices.
RestTemplate - provides load balancing feature, which load balances the request. But if I have a service running in 3 nodes, does RestTemplate knows if one node is down or responding and "intelligently" load balance between just two of it.
Using DiscoveryClient to get a service instance and make request (as demonstrated above). In this case, though not load balanced, I think the service instance returned is responsive.
The latter loses load balancing feature but provides an active service instance
The former load balances but the resultant instance may be inactive.
I am wondering to which is the preferred one to use?
Please correct me if my understanding above is incorrect.

The first option of using resttemplate is a better option
We just need to annotate the resttemplate with
#LoadBalanced and have a zuul proxy server as the edge server. If we do that then any request to the edge server will be load balanced by default with ribbon and the resttemplate will route the request in a round robin fashion.
If we use a Discoverclient then we cannot route teh request across various instances.

Related

Scaling web sockets with Spring Boot

I'm considering the performance implications of trying to horizontally scale a server which offers websocket connections to clients.
My current implementation of the server uses Spring Boot on the backend, and uses pure web sockets (without STOMP) to transport messages between the server and client.
In a world of containers and horizontal scaling, how should I design the server that it does not matter which replica the client connects to, that delivery of messages is guaranteed?
I am considering using Redis Pub/Sub for this, however, unsure what it should look like.
Currently, my code looks like the following:
#Configuration
#EnableWebSocket
public class WebSocketServerConfiguration implements WebSocketConfigurer {
#Autowired
protected ControllerHandler webSocketHandler;
#Autowired
private AuthHandshakeInterceptor interceptor;
#Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(webSocketHandler, "/ws")
.addInterceptors(interceptor)
.setAllowedOriginPatterns("*")
.withSockJS();
}
}
And the webSocketHandler looks like:
#Component
public class ControllerHandler extends TextWebSocketHandler {
#Autowired
private InitController initController;
#Autowired
private ProjectController projectController;
#Autowired ObjectMapper objectMapper;
#Override
protected void handleTextMessage(WebSocketSession session, TextMessage jsonTextMessage) throws Exception {
// get the message from a controller
session.sendMessage(new TextMessage("foo"));
}
}
I am guessing that in the controller handler, I'll need to publish a message to Redis, and that there should also be a subscriber to actually send the message across the websocket?

Is it possible to run SpringFox' swagger and swagger-ui on a different port than the main application?

We are using SpringBoot and SpringFox using #EnableSwagger2 to expose the swagger-ui.html API documentation (we don't need it to automate client code, just as documentation and test ui).
Is it possible to run all swagger related endpoints under a different port (for example the spring boot management/monitoring port) than the main application?
I researched a bit, but did not find a way in swagger's/springfox' configuration to do it. Is there a spring way to do this?
Yes, there is a Spring way of doing this:
Step 1. Adding an additional Tomcat connector
To add a port to the embedded server an additional connector needs to be configured.
We will do it by providing custom WebServerFactoryCustomizer:
#Component
public class TomcatContainerCustomizer implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
#Value("${swagger.port}")
private int swaggerPort;
#Override
public void customize(TomcatServletWebServerFactory factory) {
Connector swaggerConnector = new Connector();
swaggerConnector.setPort(swaggerPort);
factory.addAdditionalTomcatConnectors(swaggerConnector);
}
}
Now Tomcat listens on two ports but it serves the same content on both of them. We need to filter it.
Step 2. Adding a filter
Adding a servlet filter is pretty straightforward with a FilterRegistrationBean.
It can be created anywhere, I added it directly to the TomcatContainerCustomizer.
#Component
public class TomcatContainerCustomizer implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
#Value("${swagger.port}")
private int swaggerPort;
#Value("${swagger.paths}")
private List<String> swaggerPaths;
#Override
public void customize(TomcatServletWebServerFactory factory) {
Connector swaggerConnector = new Connector();
swaggerConnector.setPort(swaggerPort);
factory.addAdditionalTomcatConnectors(swaggerConnector);
}
#Bean
public FilterRegistrationBean<SwaggerFilter> swaggerFilterRegistrationBean() {
FilterRegistrationBean<SwaggerFilter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new SwaggerFilter());
filterRegistrationBean.setOrder(-100);
filterRegistrationBean.setName("SwaggerFilter");
return filterRegistrationBean;
}
private class SwaggerFilter extends OncePerRequestFilter {
private AntPathMatcher pathMatcher = new AntPathMatcher();
#Override
protected void doFilterInternal(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
FilterChain filterChain) throws ServletException, IOException {
boolean isSwaggerPath = swaggerPaths.stream()
.anyMatch(path -> pathMatcher.match(path, httpServletRequest.getServletPath()));
boolean isSwaggerPort = httpServletRequest.getLocalPort() == swaggerPort;
if(isSwaggerPath == isSwaggerPort) {
filterChain.doFilter(httpServletRequest, httpServletResponse);
} else {
httpServletResponse.sendError(404);
}
}
}
}
The properties swagger.port and swagger.paths are configured in the application.yaml:
server.port: 8080
swagger:
port: 8088
paths: |
/swagger-ui.html,
/webjars/springfox-swagger-ui/**/*,
/swagger-resources,
/swagger-resources/**/*,
/v2/api-docs
So far so good: the swagger-ui is served on the port 8088, our api on the 8080.
But there is a problem: when we try to connect to the api from the swagger-ui,
the requests are sent to the 8088 instead of 8080.
Step 3. Adjusting SpringFox config.
Swagger assumes that the api runs on the same port as the swagger-ui.
We need to explicitly specify the port:
#Value("${server.port}")
private int serverPort;
#Bean
public Docket docket() {
return new Docket(DocumentationType.SWAGGER_2)
.host("localhost:" + serverPort);
}
And the last problem: as the ui runs on a different port than the api,
the requests are considered cross-origin. We need to unblock them.
It can be done globally:
#Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**/*").allowedOrigins("http://localhost:" + swaggerPort);
}
};
}
or by adding annotations to the controllers:
#CrossOrigin(origins = "http://localhost:${swagger.port}")
Versions used: SpringBoot 2.2.2.RELEASE, springfox-swagger2 2.9.2
For a working example see https://github.com/mafor/swagger-ui-port
I don't think so. When you're setting the Spring Boot management port (management.server.port), a second application server gets started to serve the actuator stuff. As far as I know there is no possibility (apart from custom actuator endpoints) to publish something on that server.
What is your use case exactly? Do you want to prevent access to Swagger in production or for non-authenticated users?

Testing Hystrix fallback through Feign API: com.netflix.client.ClientException: Load balancer does not have available server for client

When testing the Hystrix fallback behavior of my Feign API, I get an error, when I expect it to succeed.
Feign interface:
This is the api to the external service.
#FeignClient(name = "book", fallback = BookAPI.BookAPIFallback.class)
public interface BookAPI {
#RequestMapping("/")
Map<String, String> getBook();
#Component
class BookAPIFallback implements BookAPI {
#Override
#RequestMapping("/")
public Map<String, String> getBook() {
Map<String, String> fallbackmap = new HashMap<>();
fallbackmap.put("book", "fallback book");
return fallbackmap;
}
}
}
Test class
This test exists just to verify fallback behavior:
#RunWith(SpringRunner.class)
#SpringBootTest(webEnvironment = NONE)
public class BookServiceClientTest {
#MockBean
RestTemplate restTemplate;// <---- #LoadBalanced bean
#Autowired
private BookServiceClient bookServiceClient;
#Before
public void setup() {
when(restTemplate.getForObject(anyString(), any()))
.thenThrow(new RuntimeException("created a mock failure"));
}
#Test
public void fallbackTest() {
assertThat(bookServiceClient.getBook())
.isEqualTo(new BookAPI.BookAPIFallback().getBook().get("book")); // <--- I thought this should work
}
}
config files
application.yml
These files show configuration that might be relevant:
feign:
hystrix:
enabled: true
test/application.yml
eureka:
client:
enabled: false
The Question
Everything works fine when running the apps.
But when running this test, I get the below error.
Naturally, it's a test, so I'm trying to bypass the lookup anyway.
java.lang.RuntimeException: com.netflix.client.ClientException: Load balancer does not have available server for client: book
at org.springframework.cloud.netflix.feign.ribbon.LoadBalancerFeignClient.execute(LoadBalancerFeignClient.java:71)
at feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:97)
What am I missing?
Addendums
Application class
#SpringBootApplication
#EnableCircuitBreaker
#EnableDiscoveryClient
#EnableFeignClients
public class LibraryApplication {
public static void main(String[] args) {
SpringApplication.run(LibraryApplication.class, args);
}
}
LibraryController
#Controller
public class LibraryController {
private final BookServiceClient bookService;
public LibraryController(BookServiceClient bookServiceClient) {
this.bookService = bookServiceClient;
}
#GetMapping("/")
String getLibrary(Model model) {
model.addAttribute("msg", "Welcome to the Library");
model.addAttribute("book", bookService.getBook());
return "library";
}
}
There are no other classes.
so! I was able to recreate the issue, thanks for adding more code, had to play about with it a tad as I was unsure what the BookClientService looked like and it wouldn't make sense for it to implement the BookAPI as that would be an internal call e.g. in your application and not an external API call with Feign.
Anyway,
I pushed my version of what you provided here.
https://github.com/Flaw101/feign-testing
The issue was resolved when I renamed the second application.yml which lives in the src/test/resources folder to application-test.yml which will merge the properties.
The issue was caused by the fact the second property source, the testing one, overrides the initial application.yml and disables hystrix, because Hystrix is disabled there is no fallback to go to and it throws the root cause of what would cause the fallback, a lack of a server to call to for the Book API. Renaming it to application-test will always be loaded into spring test contexts. You could resolve it with the use of inlined properties or profiles.
I've added another test disabling feign /w hystrix within the test which re-creates the error you are recieving.

Apache Camel Spring Javaconfig Unit Test No consumers available on endpoint

I have the following route configuration:
#Component
public class MyRoute extends RouteBuilder {
#Override
public void configure() throws Exception {
from("direct:in").to("direct:out");
}
}
When I try to test it:
#RunWith(CamelSpringJUnit4ClassRunner.class)
#ContextConfiguration(classes = { MyRouteTest.TestConfig.class }, loader = CamelSpringDelegatingTestContextLoader.class)
#MockEndpoints
public class MyRouteTest {
#EndpointInject(uri = "mock:direct:out")
private MockEndpoint mockEndpoint;
#Produce(uri = "direct:in")
private ProducerTemplate producerTemplate;
#Configuration
public static class TestConfig extends SingleRouteCamelConfiguration {
#Bean
#Override
public RouteBuilder route() {
return new MyRoute();
}
}
#Test
public void testRoute() throws Exception {
mockEndpoint.expectedBodiesReceived("Test Message");
producerTemplate.sendBody("Test Message");
mockEndpoint.assertIsSatisfied();
}
}
I get this exception:
org.apache.camel.component.direct.DirectConsumerNotAvailableException:
No consumers available on endpoint: Endpoint[direct://out].
Exchange[Message: Test Message]
It looks like the Mock is not picking up the message from the endpoint.
What am I doing wrong?
The problem is that mock endpoints just intercept the message before delegating to the actual endpoint. Quoted from the docs:
Important: The endpoints are still in action. What happens differently
is that a Mock endpoint is injected and receives the message first and
then delegates the message to the target endpoint. You can view this
as a kind of intercept and delegate or endpoint listener.
The solution to your problem is to tell certain endpoints (the ones that expect a consumer in your case) not to delegate to the actual endpoint. This can easily be done using #MockEndpointsAndSkip instead of #MockEndpoints:
#RunWith(CamelSpringJUnit4ClassRunner.class)
#ContextConfiguration(classes = { MyRouteTest.TestConfig.class }, loader = CamelSpringDelegatingTestContextLoader.class)
#MockEndpointsAndSkip("direct:out") // <-- turns unit test from red to green ;)
public class MyRouteTest {
// ....
}
This issue because, in your route configuration, there is no route with "direct:out" consumer endpoint.
add a line like some thing below,
from("direct:out").("Anything you want to log");
So that direct:out will consume the exchange and In your test, mock will be able check the received text without any issues. Hope this helps !!

Problems injecting a BayeuxService into another class with annotations

I have a web app that is using Bayeux to handle Comet connections. I initialize a BayeuxServer and tie it into Spring annotations and it all works fine, listening on selected channels and responding.
I have a Jersey annotated class and an annotated Bayeux service as shown below. The idea is I wanted to be able to control resources via Rest from an individual web app, and then right after the resource is changed, do a server push via Comet to all other applicable clients to tell them to update their information.
Here is the problem: A Bayeux Service is created when the webapp is deployed, setting up proper channels to listen on and monitoring clients. There should only be one instance of this. When Jersey attempts to use the Bayeux service it creates a whole new service, when it should be using the original one. This new service doesn't have the BayeuxServer properly injected so I can't access client information through it.
It makes since that this should be doable, but I don't seem to understand how to inject these things properly via annotations. Can anyone point me in the right direction?
Jersey Annotated Class:
#Path("JsonTest")
public class JsonTest {
#Context
Request request;
#Context
UriInfo uriInfo;
#Context
ResourceContext resourceContext;
protected final Logger log = Logger.getLogger(getClass());
public JsonTest() {
}
#DELETE
#Path("{id}")
public void deleteJson(#PathParam("id") String id) {
JsonTestDao.instance.getModel().remove(id);
log.info("Deleted Json..." + id);
log.info("New json: " + JsonTestDao.instance.getModel().toString());
JsonTestService jsonTestService = resourceContext.getResource(JsonTestService.class);
jsonTestService.sendUpdate();
}
}
BayeuxService:
#Named
// Singleton here didn't seem to make a difference
#Service
public class JsonTestService {
protected final Logger log = Logger.getLogger(getClass());
#Inject
private BayeuxServer bayeux;
#Session
private ServerSession serverSession;
#PostConstruct
public void init() {
log.info("Initializing JsonTest Bayeux HelloService...");
log.info("Current sessions are: " + bayeux.getSessions().toString());
}
#Listener("/cometd/JsonTest")
public void jsonTestHandler(ServerSession remote, ServerMessage.Mutable message) {
}
public void sendUpdate() {
//bayeux.newMessage(); // Need a method that the Jersey class can call to notify changes
log.info("Bayeux server should be sending an update now...");
}
#PreDestroy
public void destroy() {
log.info("Destroying JsonTest Bayeux HelloService...");
}
}
See Jersey and spring integration - bean Injections are null at runtime.
Another question I asked. Both of these stem from the same problem involving properly setting the Jersey dependency and integrating it with spring.

Resources