I have problem with customizing API gateway domain, for my restful app deployed on AWS lambda. Customized domain, works this way, that depending on basePath it chooses different APIs which finally touches Lambda. For example:
api.mycustomdomain.com/view/ping -> goes to application view with path /view/ping
api.mycustomdomain.com/admin/ping -> goes to application admin with path /admin/ping
I am using this example as boilerplate: https://github.com/awslabs/aws-serverless-java-container/tree/master/samples/spring/pet-store
What I would like to achieve is handler which depending on Host header strips prefix from request path.
I have prepared following application.yml file:
server:
contextPath: "/view"
productionHost: "api.mycustomdomain.com"
The problem/question is. How can I now load those into my Lambda function? Here is my naive try:
public class LambdaHandler implements RequestHandler<AwsProxyRequest, AwsProxyResponse> {
SpringLambdaContainerHandler<AwsProxyRequest, AwsProxyResponse> handler;
boolean isinitialized = false;
#Value("${server.contextPath}")
private String prefix;
#Value("${server.productionHost}")
private String productionHost;
public AwsProxyResponse handleRequest(AwsProxyRequest awsProxyRequest, Context context) {
if(awsProxyRequest.getHeaders().get("Host").equals(productionHost))
awsProxyRequest.setPath(awsProxyRequest.getPath().substring(prefix.length()));
if (!isinitialized) {
isinitialized = true;
try {
handler = SpringLambdaContainerHandler.getAwsProxyHandler(PingPongApp.class);
} catch (ContainerInitializationException e) {
e.printStackTrace();
return null;
}
}
return handler.proxy(awsProxyRequest, context);
}
}
Obviously this doesn't work, LambdaHandler is working out of Spring context.
Any ideas how can I deal with that?
It seems you can not load those properties. Follow either of the 2 options given below.
1> You can add following bean in your configuration and that way you can autowire strings and use the way you are already using
#Bean
public static PropertySourcesPlaceholderConfigurer propertyConfigInDev() {
return new PropertySourcesPlaceholderConfigurer();
}
2>
public AwsProxyResponse..{
#Autowired
private Environment env;
..
public AwsProxyResponse handleRequest{
..
String contextPath = env.getRequiredProperty(“server.contextPath”));
...
}
}
Related
I have an application with management.server enabled:
management.server.port=8081
When I start application, I have:
10 threads for 8080 HTTP nio connector
10 threads for 8081 HTTP nio connector
But I would like to reduce min-spare only for management (8081) and not for the web application (8080)
Looking at Spring code, it seems it's not possible, can someone confirm ?
EDIT: The approach below is not sufficient as the ManagementWebServerFactoryCustomizer is also a ConfigurableWebServerFactory and will thus be applied to the main server.
Adding logic to check againgst the management port is not helping as the management context has its very own wiring and won't pick up the bean.
Looks like it's not possible to hook into the management server configuration easily (would be easier if ServletManagementContextFactory were public).
You can look into ServletManagementChildContextConfiguration to see how the management server is wired.
You could hook into the management server configuration by providing a ManagementWebServerFactoryCustomizer like this (not sure if there's an easier way):
#Configuration
public class TomcatManagementCustomizerConfiguration {
#Bean
ManagementWebServerFactoryCustomizer<ConfigurableServletWebServerFactory> servletManagementWebServerFactoryCustomizer(
#Value("${management.server.threads.min-spare:5}") int managementMinSpareThreads,
ListableBeanFactory beanFactory) {
return new TomcatManagementCustomizer(beanFactory, managementMinSpareThreads);
}
static class TomcatManagementCustomizer extends ManagementWebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {
private final int managementMinSpareThreads;
protected TomcatManagementCustomizer(ListableBeanFactory beanFactory, int managementMinSpareThreads) {
super(beanFactory, TomcatWebServerFactoryCustomizer.class);
this.managementMinSpareThreads = managementMinSpareThreads;
}
#Override
#SuppressWarnings("rawtypes")
protected void customize(ConfigurableServletWebServerFactory factory, ManagementServerProperties managementServerProperties, ServerProperties serverProperties) {
super.customize(factory, managementServerProperties, serverProperties);
((TomcatServletWebServerFactory) factory).addConnectorCustomizers((connector) -> {
ProtocolHandler handler = connector.getProtocolHandler();
if (handler instanceof AbstractProtocol) {
AbstractProtocol protocol = (AbstractProtocol) handler;
protocol.setMinSpareThreads(managementMinSpareThreads);
}
});
}
}
}
Can you not just put the following in either properties file or YAML file?
Or is there something I misunderstood?
server.tomcat.threads.min-spare=2
(This is for properties file)
Just to verify (You don't need this as you have been checking the updated value in the log)
Put the following in either properties file or YAML file
management.endpoints.web.exposure.include=health,info,metrics,env
(This is for properties file)
And visit /actuator/env/server.tomcat.threads.min-spare
You need actuator dependency for the link above to work.
You can use #ManagementConfigurationContext and add the configuration class to to your META-INF/spring.properties file.
It is also important to place the configuration class in a package which is not the main package or sub-package of your main application context. This is so that this configuration only applies to the management context.
Below is the sampel configuration following #Holgzn's response.
#ManagementContextConfiguration
public class TomcatManagementCustomizerConfiguration {
#Bean
ManagementWebServerFactoryCustomizer<ConfigurableServletWebServerFactory> servletManagementWebServerFactoryCustomizer(
#Value("${management.server.threads.min-spare:5}") int managementMinSpareThreads,
ListableBeanFactory beanFactory) {
return new TomcatManagementCustomizer(beanFactory, managementMinSpareThreads);
}
static class TomcatManagementCustomizer extends ManagementWebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {
private final int managementMinSpareThreads;
protected TomcatManagementCustomizer(ListableBeanFactory beanFactory, int managementMinSpareThreads) {
super(beanFactory, TomcatWebServerFactoryCustomizer.class);
this.managementMinSpareThreads = managementMinSpareThreads;
}
#Override
#SuppressWarnings("rawtypes")
protected void customize(ConfigurableServletWebServerFactory factory, ManagementServerProperties managementServerProperties, ServerProperties serverProperties) {
super.customize(factory, managementServerProperties, serverProperties);
((TomcatServletWebServerFactory) factory).addConnectorCustomizers((connector) -> {
ProtocolHandler handler = connector.getProtocolHandler();
if (handler instanceof AbstractProtocol) {
AbstractProtocol protocol = (AbstractProtocol) handler;
protocol.setMinSpareThreads(managementMinSpareThreads);
}
});
}
}
}
The spring.properties file
org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration=<package>.TomcatManagementCustomizerConfiguration
Background:
I am working on a java Spring REST microservice that needs to work with multiple identical back-end systems and multiple identical databases depending on the request parameters.
Basically I have 3 "brands". For each brand there is a set of downstream services and a database. I have no control over those.
My spring service will receive brand as a part of request and will need to call the right downstream services and use the correct database.
Previously I would deal with this by having a separate instance of the spring service for each of the brands. There would be a single property file for each brand and spring would use it to wire up beans. I would have separate URL's for each brand and there was no problem.
Some of my beans need to know about "brand" during creation as they are wrappers around connections downstream services. I.e. once the bean is created there won't be a way to switch it to be a "different brand".
Problem:
I would like to change this so that a single instance of my service can handle requests for any brand.
Requirements:
I was thinking about the following solution:
Have a general property file for non-branded stuff. Spring would wire any non-branded beans and keep them as singleton beans.
Have a property file with brand specific urls etc for each of the brands
Spring would create set of singleton beans for each of the brand using appropriate property file.
Next when the request comes in spring would read the request params and use bean specific for that brand.
Performance is important to me so I would like to reuse the beans as much as possible.
I would like to make this thing as transparent as possible so that people creating new beans don't have to worry about doing anything outside standard configuration/context class.
Does anyone know what would be the best solution to achieve this?
I think you can solve the problem injecting the service in every request with the right set of configurations and beans; possibly already existing in your Application Context.
Given:
$ curl http://localhost:8080/greetings/rodo && echo
Hi from brand1, rodo
$ curl -H "x-brand-name: brand1" http://localhost:8080/greetings/rodo
Hi from brand1, rodo
$ curl -H "x-brand-name: brand2" http://localhost:8080/greetings/rodo && echo
Hi from brand2, rodo
The following code would work:
-- application.yml --
brand1:
greetingPrefix: Hi from brand1,
brand2:
greetingPrefix: Hi from brand2,
-- DemoApplication.java --
#SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
#Configuration
class ServiceConfig {
#Bean
public GreetingService greetingServiceBrand1(Brand1Config config) {
return new GreetingService(config);
}
#Bean
public GreetingService greetingServiceBrand2(Brand2Config config) {
return new GreetingService(config);
}
}
#Configuration
class WebConfig implements WebMvcConfigurer {
#Autowired
private ApplicationContext applicationContext;
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(greetingServiceResolver());
}
private GreetingServiceResolver greetingServiceResolver() {
GreetingService greetingServiceBrand1 = applicationContext.getBean("greetingServiceBrand1", GreetingService.class);
GreetingService greetingServiceBrand2 = applicationContext.getBean("greetingServiceBrand2", GreetingService.class);
return new GreetingServiceResolver(greetingServiceBrand1, greetingServiceBrand2);
}
}
}
#RestController
#RequestMapping("/greetings")
class GreetingController {
#GetMapping("/{name}")
public String get(GreetingService greetingService, #PathVariable String name) {
return greetingService.sayHi(name);
}
}
class GreetingServiceResolver implements HandlerMethodArgumentResolver {
private final GreetingService greetingServiceBrand1;
private final GreetingService greetingServiceBrand2;
public GreetingServiceResolver(GreetingService greetingServiceBrand1, GreetingService greetingServiceBrand2) {
this.greetingServiceBrand1 = greetingServiceBrand1;
this.greetingServiceBrand2 = greetingServiceBrand2;
}
#Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType().equals(GreetingService.class);
}
#Override
public Object resolveArgument(
MethodParameter methodParameter,
ModelAndViewContainer modelAndViewContainer,
NativeWebRequest nativeWebRequest,
WebDataBinderFactory webDataBinderFactory
) throws Exception {
String brand = nativeWebRequest.getHeader("x-brand-name");
return resolveGreetingService(brand);
}
private GreetingService resolveGreetingService(String brand) {
if ("brand2".equals(brand)) {
return greetingServiceBrand2;
}
return greetingServiceBrand1; // default
}
}
class GreetingService {
private BaseConfig config;
public GreetingService(BaseConfig config) {
this.config = config;
}
public String sayHi(String name) {
return config.getGreetingPrefix() + " " + name;
}
}
abstract class BaseConfig {
private String greetingPrefix;
public String getGreetingPrefix() {
return greetingPrefix;
}
public void setGreetingPrefix(String greetingPrefix) {
this.greetingPrefix = greetingPrefix;
}
}
#Configuration
#ConfigurationProperties("brand1")
class Brand1Config extends BaseConfig {
}
#Configuration
#ConfigurationProperties("brand2")
class Brand2Config extends BaseConfig {
}
As you can see, it's fundamental to pass the service to each controller method, write a resolver and inject the right set of dependencies depending on a parameter passed to the request, in this case via header.
Since your property files need to be declared statically anyway, you can just write all your different brand stuff in the same property file, like in a key-value format, that Spring can pick up as a list of configurations.
brandConfigs:
- brand: foo
property: foos
- brand2: bar
porperty: bars
Load all your connection beans to your downstream services on startup and just route to them according to your request param. Imo this seems to be the most straight forward and performant way. If some of these downstreams are used very rarely you can lazy load the beans on-demand, but probably this wouldn't make a sense unless you have thousands of different downstream routes.
I'm trying to change base access url for API documentation. The url is "http://localhost:8080/swagger-ui.html". I want to get something like "http://localhost:8080/myapi/swagger-ui.html".
I use Springfox 2.8.0 Swagger, Java 8, Spring Boot 2.0
The swagger configuration is:
#Configuration
#EnableSwagger2
public class SwaggerConfiguration {
#Bean
public Docket api(ServletContext servletContext) {
return new Docket(DocumentationType.SWAGGER_2)
.pathProvider(new RelativePathProvider(servletContext) {
#Override
public String getApplicationBasePath() {
return "/myapi";
}
})
.select()
.apis(RequestHandlerSelectors.any())
.paths(Predicates.not(PathSelectors.regex("/error")))
.build()
.useDefaultResponseMessages(false);
}
}
Custom path provider had to help, but I still get access to api documentation by using url "http://localhost:8080/swagger-ui.html". If I use url "http://localhost:8080/myapi/swagger-ui.html", I get 404 error. Look at the screenshot below.
UPD: Springfox is abandoned
Springfox Swagger had always been kinda dirty solution with a lot of unclearness and bugs, but by now (2021 Q4) it hadn't been updated for more than a year.
The final straw was the fact that Springfox Swagger 3.0 doesn't work anymore with Spring Boot 2.6.x.
So, if you reading this, please, consider switching over to https://springdoc.org/ instead.
It's a pretty straightforward conversion and they do a great job of
documenting it. https://springdoc.org/#migrating-from-springfox.
For those who use Springfox Swagger 3.0.0
Here's the working configuration for changing base url for docs:
springfox:
documentation:
swaggerUi:
baseUrl: /documentation
openApi:
v3:
path: /documentation/v3/api-docs
swagger:
v2:
path: /documentation/v2/api-docs
You can edit your SwaggerConfiguration like that:
Take care to replace the package (which need to be the one
containing your REST controllers), the host, and the PATH you need
#Configuration
#EnableSwagger2
public class SwaggerConfiguration implements WebMvcConfigurer {
public static final String PATH = "/myapi";
#Bean
public Docket api() {
final var package = "com.julia.rest";
final var host = "localhost:8080";
return new Docket(DocumentationType.SWAGGER_2)
.host(host)
.select()
.apis(RequestHandlerSelectors.basePackage(package))
.paths(PathSelectors.any())
.build();
}
#Override
public void addViewControllers(ViewControllerRegistry registry) {
final var apiDocs = "/v2/api-docs";
final var configUi = "/swagger-resources/configuration/ui";
final var configSecurity = "/swagger-resources/configuration/security";
final var resources = "/swagger-resources";
registry.addRedirectViewController(PATH + apiDocs, apiDocs).setKeepQueryParams(true);
registry.addRedirectViewController(PATH + resources, resources);
registry.addRedirectViewController(PATH + configUi, configUi);
registry.addRedirectViewController(PATH + configSecurity, configSecurity);
registry.addRedirectViewController(PATH, "/");
}
#Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler(PATH + "/**").addResourceLocations("classpath:/META-INF/resources/");
}
}
Another solution is by changing the spring-boot URL context-path:
Edit pour application.properties file:
server.servlet.context-path=/myapi
Or if you have an application.yml file:
server:
servlet:
context-path: /myapi
Warning: It will change the base path of all your web services, not only Swagger
I also have faced this problem and tried many possible resolutions, and nothings didn't help really.
In my case, I can't use any resource redirect as swagger must be accessible as locally as on google cloud by match path /api-docs/**. and on google cloud any resource redirection will be denied in my case. All resources must be loading also from this path
here is my solution:
springfox-swagger2 and springfox-swagger-ui of version 2.9.2
#EnableSwagger2
#Configuration
public class SwaggerCommonConfig implements WebMvcConfigurer {
public static final String PATH = "/api-docs";
#Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.any())
.paths(PathSelectors.any())
.build();
}
#Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addRedirectViewController(PATH, "/");
}
#Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler(PATH + "/**").addResourceLocations("classpath:/META-INF/resources/");
}
}
and as springfox don't have any possibilities to do it by another way, in my case, we just will create simple controller that will be translating resource requests from our custom path to standard springfox. (it's not very elegant part but as it is :))
#RestController
#RequestMapping(SwaggerGatewayCommonConfig.PATH)
#RequiredArgsConstructor
public class SwaggerController {
private final RestTemplate restTemplate;
private final static String V2_API_DOCS = "/v2/api-docs";
private final static String SWAGGER_RESOURCES_CONFIGURATION_UI = "/swagger-resources/configuration/ui";
private final static String SWAGGER_RESOURCES_CONFIGURATION_SECURITY = "/swagger-resources/configuration/security";
private final static String SWAGGER_RESOURCES = "/swagger-resources";
private final static Pattern pattern = Pattern.compile("http[s]*://([^/]+)", Pattern.CASE_INSENSITIVE);
#Value("${server.port}")
private String port;
#GetMapping(V2_API_DOCS)
#SuppressWarnings("unchecked")
public Map<String, Object> getV2ApiDocs(HttpServletRequest request) {
Matcher matcher = pattern.matcher(request.getRequestURL().toString());
matcher.find();
Map<String, Object> resp = (Map<String, Object>) restTemplate.getForObject(toLocalSwaggerUrl(V2_API_DOCS), Map.class);
//we have to replace standard host, to requested host. as swagger UI make api requests from this host
resp.put("host", matcher.group(1));
return resp;
}
#GetMapping(SWAGGER_RESOURCES_CONFIGURATION_UI)
public Object getSwaggerResourcesConfigurationUi() {
return restTemplate.getForObject(toLocalSwaggerUrl(SWAGGER_RESOURCES_CONFIGURATION_UI), Object.class);
}
#GetMapping(SWAGGER_RESOURCES_CONFIGURATION_SECURITY)
public Object getSwaggerResourcesConfigurationSecurity() {
return restTemplate.getForObject(toLocalSwaggerUrl(SWAGGER_RESOURCES_CONFIGURATION_SECURITY), Object.class);
}
#GetMapping(SWAGGER_RESOURCES)
public Object getSwaggerResources() {
return restTemplate.getForObject(toLocalSwaggerUrl(SWAGGER_RESOURCES), Object.class);
}
private String toLocalSwaggerUrl(String path) {
return "http://localhost:" + port + path;
}
}
I hope it will save time to somebody faced it also =)
Good luck
Swagger base access url is constructed from your base application path.So if you change your base application path , you will get the desired behavior.But also all your apis will be changed to that path. You can find how to change it here How to set base url for rest in spring boot? .
What you did was too change how swagger call other apis from your application, not to change his base url. There are some tricks to change the swagger base url without changing application base path (moving manually all swagger resources), but i do not recommend that.
I'm trying to use a component to load up my configuration yml file.
However, it throws a Null Pointer exception and System.out shows 'null' for application.
However, when the same pattern is used to code up a #RestController, everything works fine. Why is #component not seeing my configurations??
#Component
#ConfigurationProperties(prefix = "myconf")
public class AppConfigJSON {
private String application;
private final String applicationConfigJSON;
Logger logger = LoggerFactory.getLogger(this.getClass());
private final ReadContext acRcJson;
public AppConfigJSON(){
String json = "";
try {
System.out.println("Application: " + this.application);
json = IOUtils.toString(this.getClass().getResourceAsStream("/myconfs/"+this.application+".json"));
} catch (IOException e) {
logger.error("Error reading JSON or app YML {}", e.getStackTrace());
}
this.applicationConfigJSON = json;
this.acRcJson = JsonPath.parse(this.getApplicationConfigJSON());
}
// These functions below set by configuration
public String getApplication()
{
return application;
}
public void setApplication(String application)
{
this.application = application;
}
}
In your constructor the application variable hasn't been initialized yet, in other words, Spring needs an instance first so it can apply it's magic. You need to move your constructor logic to a method annotated with #PostContruct so the application variable is set with the property value at that point.
I know you can set the server.contextPath in application.properties to change the root context.
Also, I can add an additional context in the application config for Spring Boot like the following example (in Groovy) to add an "/api" to the URL mappings of the root context:
#Bean
ServletRegistrationBean dispatcherServlet() {
ServletRegistrationBean reg = new ServletRegistrationBean(new DispatcherServlet(), "/")
reg.name = "dispatcherServlet"
reg.addInitParameter("contextConfigLocation", "")
reg.addUrlMappings("/api/*")
reg.loadOnStartup = 2
reg
}
}
I am trying to have a separate base URI "/api" specifically for web service calls, that I can leverage for security, etc. However using the above approach will mean that any of my URIs, web service or not, can be reached with "/" or "/api", and provides no concrete segregation.
Is anyone aware of a better approach to set a base path for all #RestController(s) using configuration, without having to formally prefix every controller with /api/? If I am forced to manually prefix the URI for each controller, it would be possible to mistakenly omit that and bypass my security measures specific to web services.
Here is a reference in Stack Overflow to the same type of question, which was never completely answered:
Spring Boot: Configure a url prefix for RestControllers
In continuation to the currently accepted solution the github issue addresses the same.
Spring 5.1 and above you can implement WebMvcConfigurer and override configurePathMatch method like below
#Configuration
#EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
#Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.addPathPrefix("/api",
HandlerTypePredicate.forAnnotation(RestController.class));
}
}
Now all the #RestControllers will have /api as the prefix path alongside the path configured.
Official Documentation
There's a new solution to solve this kind of problem available since Spring Boot 1.4.0.RC1 (Details see https://github.com/spring-projects/spring-boot/issues/5004)
The solution of Shahin ASkari disables parts of the Auto configuration, so might cause other problems.
The following solution takes his idea and integrates it properly into spring boot. For my case I wanted all RestControllers with the base path api, but still serve static content with the root path (f.e. angular webapp)
Edit: I summed it up in a blog post with a slightly improved version see https://mhdevelopment.wordpress.com/2016/10/03/spring-restcontroller-specific-basepath/
#Configuration
public class WebConfig {
#Bean
public WebMvcRegistrationsAdapter webMvcRegistrationsHandlerMapping() {
return new WebMvcRegistrationsAdapter() {
#Override
public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
return new RequestMappingHandlerMapping() {
private final static String API_BASE_PATH = "api";
#Override
protected void registerHandlerMethod(Object handler, Method method, RequestMappingInfo mapping) {
Class<?> beanType = method.getDeclaringClass();
RestController restApiController = beanType.getAnnotation(RestController.class);
if (restApiController != null) {
PatternsRequestCondition apiPattern = new PatternsRequestCondition(API_BASE_PATH)
.combine(mapping.getPatternsCondition());
mapping = new RequestMappingInfo(mapping.getName(), apiPattern,
mapping.getMethodsCondition(), mapping.getParamsCondition(),
mapping.getHeadersCondition(), mapping.getConsumesCondition(),
mapping.getProducesCondition(), mapping.getCustomCondition());
}
super.registerHandlerMethod(handler, method, mapping);
}
};
}
};
}
}
Also You can achieve the same result by configuring WebMVC like this:
#Configuration
public class PluginConfig implements WebMvcConfigurer {
public static final String PREFIX = "/myprefix";
#Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.addPathPrefix(PREFIX, c -> c.isAnnotationPresent(MyCustomAnnotation.class));
}
}
Implement WebMvcConfigurer on any #Configuration class.
Override configurePathMatch method.
You can do many useful things with PathMatchConfigurer e.g. add prefix for several classes, that satisfy predicate conditions.
I had the same concern and was not a fan of the Spring EL option due to the issues documented and I wanted the prefix to be tightly controlled in the controllers but I did not want to depend on the developers doing the right thing.
There might be a better way these days but this is what I did. Can you guys see any downsides, I am still in the process of testing any side-effects.
Define a custom annotation.
This allows a developer to explicitly provide typed attributes such as int apiVersion(), String resourceName(). These values would be the basis of the prefix later.
Annotated rest controllers with this new annotation
Implemented a custom RequestMappingHandlerMapping
In the RequestMappingHandlerMapping, I could read the attribute of the custom annotation and modify the final RequestMappingInfo as I needed. Here are a few code snippets:
#Configuration
public class MyWebMvcConfigurationSupport extends WebMvcConfigurationSupport {
#Bean
public RequestMappingHandlerMapping requestMappingHandlerMapping() {
return new MyCustomRequestMappingHandlerMapping();
}
}
And in the MyCustomRequestMappingHandlerMapping, overwrite the registerHandlerMethod:
private class MyCustomRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
private Logger myLogger = LoggerFactory.getLogger(MyCustomRequestMappingHandlerMapping.class);
public MyCustomRequestMappingHandlerMapping() {
super();
}
#Override
protected void registerHandlerMethod(Object handler, Method method, RequestMappingInfo mapping) {
// find the class declaring this method
Class<?> beanType = method.getDeclaringClass();
// check for the My rest controller annotation
MyRestController myRestAnnotation = beanType.getAnnotation(MyRestController.class);
if (myRestAnnotation != null) {
// this is a My annotated rest service, lets modify the URL mapping
PatternsRequestCondition oldPattern = mapping.getPatternsCondition();
// create a pattern such as /api/v${apiVersion}/${resourceName}
String urlPattern = String.format("/api/v%d/%s",
myRestAnnotation.apiVersion(),
myRestAnnotation.resourceName());
// create a new condition
PatternsRequestCondition apiPattern =
new PatternsRequestCondition(urlPattern);
// ask our condition to be the core, but import all settinsg from the old
// pattern
PatternsRequestCondition updatedFinalPattern = apiPattern.combine(oldPattern);
myLogger.info("re-writing mapping for {}, myRestAnnotation={}, original={}, final={}",
beanType, myRestAnnotation, oldPattern, updatedFinalPattern);
mapping = new RequestMappingInfo(
mapping.getName(),
updatedFinalPattern,
mapping.getMethodsCondition(),
mapping.getParamsCondition(),
mapping.getHeadersCondition(),
mapping.getConsumesCondition(),
mapping.getProducesCondition(),
mapping.getCustomCondition()
);
}
super.registerHandlerMethod(handler, method, mapping);
}
}
Slightly less verbose solution which doesn't duplicate the logic of checking the annotation, but only changes the mapping path:
private static final String API_PREFIX = "api";
#Bean
WebMvcRegistrationsAdapter restPrefixAppender() {
return new WebMvcRegistrationsAdapter() {
#Override
public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
return new RequestMappingHandlerMapping() {
#Override
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
RequestMappingInfo mappingForMethod = super.getMappingForMethod(method, handlerType);
if (mappingForMethod != null) {
return RequestMappingInfo.paths(API_PREFIX).build().combine(mappingForMethod);
} else {
return null;
}
}
};
}
};
}
Side effects
Your error controller will also be mapped under /api/error, which breaks error handling (DispatcherServlet will still redirect errors to /error without prefix!).
Possible solution is to skip /error path when adding /api prefix in the code above (one more "if").
Someone has filed an issue in the Spring MVC Jira and come up with a nice solution, which I am now using. The idea is to use the Spring Expression Language in the prefix placed in each RestController file and to refer to a single property in the Spring Boot application.properties file.
Here is the link of the issue: https://jira.spring.io/browse/SPR-13882