How to use Protobuf and JSON API simultaneously on SpringBoot? - spring

My project only had Protobuf APIs, but now it has to provide JSON APIs, too.
protobufHttpMessageConverter was applied to all controllers by configuration.
#Configuration
class APIConfig : WebMvcConfigurer {
#Bean
fun protobufHttpMessageConverter(): ProtobufHttpMessageConverter {
val converter = ProtobufHttpMessageConverter()
converter.defaultCharset = StandardCharsets.UTF_8
return converter
}
}
But now it should be applied to some controllers that provide Protobuf APIs, and should not be applied to other controllers that provide JSON APIs.
Does SpringBoot have that kind of flexibility?

Related

Managing multiple routes in Spring Cloud Gateway

I know Spring Cloud Gateway has multiple ways to configure routes:
using a Java-based DSL (eg: using RouteLocatorBuilder) and/or
property based configuration.
The offical Spring Cloud Gateway docs use properties to manage routes.
My questions are:
It is simple to configure routes for 2-3 microservices in a single file, but how do enterprise applications with so many microservices manage routes efficiently?
What is the recommend way to configure routes?
If using Java DSL, is it a good practice to use multiple Beans with the same return type. something like:
#Bean
RouteLocator bookRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("book_route", r -> r.method(HttpMethod.GET)
.and().path("/api/book/**")
.uri("lb://book-service"))
.build();
}
#Bean
RouteLocator chapterRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("chapter_route", r -> r.method(HttpMethod.GET)
.and().path("/api/chapter/**")
.uri("lb://chapter-service"))
.build();
}
This is a decently old post but this is something I recently ran into and wanted to share a solution.
Exposing multiple beans will not work - those two RouteLocator instances will collide with each other causing an error or you can allow spring to override the other bean - either way, its not going to work for you.
Instead, expose a Router that references other classes that expose functions that create your routes. Here is an example in Kotlin
Routes.kt
#Configuration
class Routes(
private val purchaseRoute: PurchaseRoute
) {
#Bean
fun router(routeLocatorBuilder: RouteLocatorBuilder): RouteLocator {
return routeLocatorBuilder.routes()
.route("purchase", purchaseRoute.route())
.build()
}
}
Router.kt interface all sub-routes will implement
interface Router {
fun route(): Function<PredicateSpec, Buildable<Route>>
}
PurchaseRoute.kt
#Configuration
class PurchaseRoute() : Router {
override fun route(): Function<PredicateSpec, Buildable<Route>> =
Function {
it.path("/api/purchase/**")
it.filters {
stripPrefix(1)
dedupeResponseHeader("Access-Control-Allow-Origin", "RETAIN_FIRST")
}
it.uri(routeConfigProperties.uri)
}
}
Then just create as many route classes as you like. The separation into multiple files dramatically cleaned up a gateway service I maintain.

Additional Media Types for Spring Data Rest Repository Controllers

Trying to add the JSON-LD serialisation provided by the project Hydra Java to an application using Spring Data Rest and the controllers generated by #RepositoryRestResource
While I've been able to serialize as JSON-LD using manually created #RepositoryRestController using the application/ld+json media type, I'm unable to do the same for the controllers generated for the repositories annotated as RepositoryRest Resource. They seem to just accept application/json and application/hal+json
Is it possible to configure these controllers to also accept additional media types?
This is how I've configured the HydraMessageConverter provided by the Hydra Java project:
#Bean
public RepositoryRestConfigurer repositoryRestConfigurer()
{
return new RepositoryRestConfigurerAdapter()
{
#Override
public void configureHttpMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
messageConverters.add(0, new HydraMessageConverter());
super.configureHttpMessageConverters(messageConverters);
}
}
}

Spring return image from controller while using Jackson Hibernate5Module

I am using Spring 4.3.1 and Hibernate 5.1.0 for my webapp.
For Jackson to be able serializing lazy objects I have to add the Hibernate5Module to my default ObjectMapper. This I have done via
#EnableWebMvc
#Configuration
#ComponentScan({ "xxx.controller" })
public class SpringWebConfig extends WebMvcConfigurerAdapter {
#Autowired
SessionFactory sf;
...
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
Hibernate5Module module = new Hibernate5Module(sf);
module.disable(Feature.USE_TRANSIENT_ANNOTATION);
module.enable(Feature.FORCE_LAZY_LOADING);
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
builder.modulesToInstall(module);
converters.add(new MappingJackson2HttpMessageConverter(builder.build()));
super.configureMessageConverters(converters);
}
}
This is working but if it is enabled serializing a byte[] does not work anymore and fails with HTTP Status 500 - Could not write content: No serializer found for class java.io.BufferedInputStream
So my question is how to extend the default ObjectMapper while preserving the default ones?
I have seen somthing preserving the defaults using Spring Boot but I do not use Spring Boot. Any ideas?
As specified in the WebMvcConfigurer.configureMessageConverters javadoc, "If no converters are added, a default list of converters is registered", i.e. you will have to manually add all the default converters if you are using WebMvcConfigurer. Calling 'super.configureMessageConverters(converters)' does nothing if you extend WebMvcConfigurer. Take a look in 'WebMvcConfigurationSupport.addDefaultHttpMessageConverters(...)' to see all the default message converters, you can also extend this class instead of WebMvcConfigurer, with which you get slightly more clarity what happens.

Spring Boot with Two MVC Configurations

I have a Spring Boot app with a REST API, using Jackson for the JSON view configuration. It works great and I can get all the Spring Boot goodness.
However, I need to add an additional REST API that is similar but with different settings. For example, among other things, it needs a different Jackson object mapper configuration because the JSON will look quite a bit different (e.g. no JSON arrays). That is just one example but there are quite a few differences. Each API has a different context (e.g. /api/current and /api/legacy).
Ideally I'd like two MVC configs mapped to these different contexts, and not have to give up any of the automatic wiring of things in boot.
So far all I've been able to get close on is using two dispatcher servlets each with its own MVC config, but that results in Boot dropping a whole bunch of things I get automatically and basically defeats the reason for using boot.
I cannot break the app up into multiple apps.
The answer "you cannot do this with Boot and still get all its magic" is an acceptable answer. Seems like it should be able to handle this though.
There's several ways to achieve this. Based on your requirement , Id say this is a case of managing REST API versions.
There's several ways to version the REST API, some the popular ones being version urls and other techniques mentioned in the links of the comments.
The URL Based approach is more driven towards having multiple versions of the address:
For example
For V1 :
/path/v1/resource
and V2 :
/path/v2/resource
These will resolve to 2 different methods in the Spring MVC Controller bean, to which the calls get delegated.
The other option to resolve the versions of the API is to use the headers, this way there is only URL, multiple methods based on the version.
For example:
/path/resource
HEADER:
X-API-Version: 1.0
HEADER:
X-API-Version: 2.0
This will also resolve in two separate operations on the controller.
Now these are the strategies based on which multiple rest versions can be handled.
The above approaches are explained well in the following: git example
Note: The above is a spring boot application.
The commonality in both these approaches is that there will need to be different POJOS based on which Jackson JSON library to automatically marshal instances of the specified type into JSON.
I.e. Assuming that the code uses the #RestController [org.springframework.web.bind.annotation.RestController]
Now if your requirement is to have different JSON Mapper i.e. different JSON mapper configurations, then irrespective of the Spring contexts you'll need a different strategy for the serialization/De-Serialization.
In this case, you will need to implement a Custom De-Serializer {CustomDeSerializer} that will extend JsonDeserializer<T> [com.fasterxml.jackson.databind.JsonDeserializer] and in the deserialize() implement your custom startegy.
Use the #JsonDeserialize(using = CustomDeSerializer.class) annotation on the target POJO.
This way multiple JSON schemes can be managed with different De-Serializers.
By Combining Rest Versioning + Custom Serialization Strategy , each API can be managed in it's own context without having to wire multiple dispatcher Servlet configurations.
Expanding on my comment of yesterday and #Ashoka Header idea i would propose to register 2 MessageConverters (legacy and current) for custom media types. You can do this like that:
#Bean
MappingJackson2HttpMessageConverter currentMappingJackson2HttpMessageConverter() {
MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter();
ObjectMapper objectMapper = new ObjectMapper();
// set features
jsonConverter.setObjectMapper(objectMapper);
jsonConverter.setSupportedMediaTypes(Arrays.asList(new MediaType("json", "v2")));
return jsonConverter;
}
#Bean
MappingJackson2HttpMessageConverter legacyMappingJackson2HttpMessageConverter() {
MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter();
ObjectMapper objectMapper = new ObjectMapper();
// set features
jsonConverter.setObjectMapper(objectMapper);
return jsonConverter;
}
Pay attention to the custom media-type for one of the converters.
If you like , you can use an Interceptor to rewrite the Version-Headers proposed by #Ashoka to a custom Media-Type like so:
public class ApiVersionMediaTypeMappingInterceptor extends HandlerInterceptorAdapter {
#Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
try {
if(request.getHeader("X-API-Version") == "2") {
request.setAttribute("Accept:","json/v2");
}
.....
}
}
This might not be the exact answer you were looking for, but maybe it can provide some inspiration. An interceptor is registered like so.
If you can live with a different port for each context, then you only have to overwrite the DispatcherServletAutoConfiguration beans. All the rest of the magic works, multpart, Jackson etc. You can configure the Servlet and Jackson/Multipart etc. for each child-context separately and inject bean of the parent context.
package test;
import static org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_BEAN_NAME;
import static org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
#Configuration
#EnableAutoConfiguration(exclude = {
Application.Context1.class,
Application.Context2.class
})
public class Application extends WebMvcConfigurerAdapter {
#Bean
public TestBean testBean() {
return new TestBean();
}
public static void main(String[] args) {
final SpringApplicationBuilder builder = new SpringApplicationBuilder().parent(Application.class);
builder.child(Context1.class).run();
builder.child(Context2.class).run();
}
public static class TestBean {
}
#Configuration
#EnableAutoConfiguration(exclude = {Application.class, Context2.class})
#PropertySource("classpath:context1.properties")
public static class Context1 {
#Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
DispatcherServlet dispatcherServlet() {
DispatcherServlet dispatcherServlet = new DispatcherServlet();
// custom config here
return dispatcherServlet;
}
#Bean(name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)
ServletRegistrationBean dispatcherServletRegistration() {
ServletRegistrationBean registration = new ServletRegistrationBean(dispatcherServlet(), "/test1");
registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME);
// custom config here
return registration;
}
#Bean
Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder(TestBean testBean) {
System.out.println(testBean);
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
// custom config here
return builder;
}
}
#Configuration
#EnableAutoConfiguration(exclude = {Application.class, Context1.class})
#PropertySource("classpath:context2.properties")
public static class Context2 {
#Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
DispatcherServlet dispatcherServlet() {
DispatcherServlet dispatcherServlet = new DispatcherServlet();
// custom config here
return dispatcherServlet;
}
#Bean(name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)
ServletRegistrationBean dispatcherServletRegistration() {
ServletRegistrationBean registration = new ServletRegistrationBean(dispatcherServlet(), "/test2");
registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME);
// custom config here
return registration;
}
#Bean
Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder(TestBean testBean) {
System.out.println(testBean);
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
// custom config here
return builder;
}
}
}
The context1/2.properties files currently only contain a server.port=8080/8081 but you can set all the other spring properties for the child contexts there.
In Spring-boot ypu can use different profiles (like dev and test).
Start application with
-Dspring.profiles.active=dev
or -Dspring.profiles.active=test
and use different properties files named application-dev.properties or application-test.properties inside your properties directory.
That could do the problem.

Spring HATEOAS (w Spring Boot) JAXB marshal error when returning a Resources<T> or PagedResources<T> result

I've got something like this in my controller:
#RequestMapping
#ResponseBody
public HttpEntity<PagedResources<PromotionResource>> promotions(
#PageableDefault(size = RestAPIConfig.DEFAULT_PAGE_SIZE, page = 0) Pageable pageable,
PagedResourcesAssembler<Promotion> assembler
){
PagedResources<PromotionResource> r = assembler.toResource(this.promoService.find(pageable), this.promoAssembler);
return new ResponseEntity<PagedResources<PromotionResource>>(r, HttpStatus.OK);
}
If i navigate to the URL mapped to that controller method i get a 500 error with a root cause of:
com.sun.istack.internal.SAXException2: unable to marshal type "commerce.api.rest.resources.PromotionResource " as an element because it is missing an #XmlRootElement annotation
If i throw a #XmlRootElement annotation on my resource it becomes this error:
com.sun.istack.internal.SAXException2: unable to marshal type "commerce.api.rest.resources.PromotionResource " as an element because it is not known to this context.
Everything is fine if the accept header is application/json or application/hal+json. The problem is caused only when the client (in this case chrome) is looking for application/xml (which makes sense as HATEOAS is following the clients requests. I'm using spring boot's #EnableAutoConfiguration which is adding the XML message converter to the list and thus enabling XML content types.
I'm guessing i have at least 2 options:
1. fix the jaxb error
2. remove xml as a supported content type
not sure how to do either, or maybe there's another option.
If you don't actually want to produce XML try using the produces attribute of the #RequestMapping annotation. Something like: #RequestMapping(produces=MediaType.APPLICATION_JSON_VALUE)
Alternatively you could exclude jaxb from you classpath or look at adding your own org.springframework.boot.autoconfigure.web.HttpMessageConverters bean to take complete control of the registered HttpMessageConverter's. See WebMvcConfigurationSupport.addDefaultHttpMessageConverters to see what Spring will add by default.
Not sure this is a good technique, and it looks like in 1.1.6 there's a different approach. Here's what i did:
#Configuration
public class WebMVCConfig extends WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter {
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
//Remove the Jaxb2 that is automatically added because some other dependency brings it into the classpath
List<HttpMessageConverter<?>> baseConverters = new ArrayList<HttpMessageConverter<?>>();
super.configureMessageConverters(baseConverters);
for(HttpMessageConverter<?> c : baseConverters){
if(!(c instanceof Jaxb2RootElementHttpMessageConverter)){
converters.add(c);
}
}
}
}
if you don't want to support XML converter, you can extend spring WebMvcConfigurer to exclude XML message converters.
#Configuration
public class WebMVCConfig extends WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter {
#Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.removeIf(c -> c instanceof AbstractXmlHttpMessageConverter<?>);
}
}

Resources