Error creating bean with name 'scopedTarget.oauth2ClientContext': Scope 'request' is not active for the current thread for feign client - spring-boot

I am calling another microservice once my current microservice is up and ready using feign client in my current microservice built using Jhipster.
So my Feign Interface is
package com.persistent.integration.client;
import java.util.List;
import org.springframework.data.domain.Pageable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import com.persistent.integration.service.dto.DataPipelineDTO;
#AuthorizedFeignClient(name = "Integrationconfiguration")
public interface DataPipelinesResourceFeign {
#RequestMapping(value = "/api/data-pipelines", method = RequestMethod.GET)
List<DataPipelineDTO> getAllDataPipelines(#RequestParam(value = "pageable") Pageable pageable );
}
}
And I have implemented ApplicationRunner where I have called feign client method.
#Component
public class ApplicationInitializer implements ApplicationRunner {
#Autowired
private DataPipelinesResourceFeign dataPipelinesResourceFeign;
#Autowired
private ActiveMQListener activeMqListener;
#Override
public void run(ApplicationArguments args) throws Exception {
// TODO Auto-generated method stub
Pageable pageable = PageRequest.of(0, 20);
try {
List <DataPipelineDTO> allStartedDataPipeLines = dataPipelinesResourceFeign.getAllDataPipelines(pageable); //.stream().filter(p->p.getState().equals(State.STARTED)).collect(Collectors.toList());
allStartedDataPipeLines.forEach(datapipe ->
{
try {
activeMqListener.consume(datapipe);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
});
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
But after running this, it gives below exception at dataPipelinesResourceFeign.getAllDataPipelines :
com.netflix.hystrix.exception.HystrixRuntimeException: DataPipelinesResourceFeign#getAllDataPipelines(Pageable) failed and no fallback available.
at com.netflix.hystrix.AbstractCommand$22.call(AbstractCommand.java:819)
at com.netflix.hystrix.AbstractCommand$22.call(AbstractCommand.java:804)
at rx.internal.operators.OperatorOnErrorResumeNextViaFunction$4.onError(OperatorOnErrorResumeNextViaFunction.java:140)
at rx.internal.operators.OnSubscribeDoOnEach$DoOnEachSubscriber.onError(OnSubscribeDoOnEach.java:87)
at rx.internal.operators.OnSubscribeDoOnEach$DoOnEachSubscriber.onError(OnSubscribeDoOnEach.java:87)
at com.netflix.hystrix.AbstractCommand$DeprecatedOnFallbackHookApplication$1.onError(AbstractCommand.java:1472)
Caused by: org.springframework.beans.factory.BeanCreationException:
Error creating bean with name 'scopedTarget.oauth2ClientContext':
Scope 'request' is not active for the current thread; consider
defining a scoped proxy for this bean if you intend to refer to it
from a singleton; nested exception is java.lang.IllegalStateException:
No thread-bound request found: Are you referring to request attributes
outside of an actual web request, or processing a request outside of
the originally receiving thread? If you are actually operating within
a web request and still receive this message, your code is probably
running outside of DispatcherServlet/DispatcherPortlet: In this case,
use RequestContextListener or RequestContextFilter to expose the
current request. at
org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(Abstrac>tBeanFactory.java:362)
at
org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractB>eanFactory.java:199)
at
org.springframework.aop.target.SimpleBeanTargetSource.getTarget(SimpleBeanTarge>tSource.java:35)
at
org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.>java:193)
at com.sun.proxy.$Proxy147.getAccessToken(Unknown Source) at
com.persistent.integration.security.oauth2.AuthorizationHeaderUtil.getAuthoriza>tionHeaderFromOAuth2Context(AuthorizationHeaderUtil.java:28)
at
com.persistent.integration.client.TokenRelayRequestInterceptor.apply(TokenRelay>RequestInterceptor.java:23)
at
feign.SynchronousMethodHandler.targetRequest(SynchronousMethodHandler.java:158)
at
feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:88)
at
feign.SynchronousMethodHandler.invoke(SynchronousMethodHandler.java:76)
at
feign.hystrix.HystrixInvocationHandler$1.run(HystrixInvocationHandler.java:108)
at com.netflix.hystrix.HystrixCommand$2.call(HystrixCommand.java:302)
at com.netflix.hystrix.HystrixCommand$2.call(HystrixCommand.java:298)
at
rx.internal.operators.OnSubscribeDefer.call(OnSubscribeDefer.java:46)
... 68 more Caused by: java.lang.IllegalStateException: No
thread-bound request found: Are you referring to request attributes
outside of an actual web request, or processing a request outside of
the originally receiving thread? If you are actually operating within
a web request and still receive this message, your code is probably
running outside of DispatcherServlet/DispatcherPortlet: In this case,
use RequestContextListener or RequestContextFilter to expose the
current request. at
org.springframework.web.context.request.RequestContextHolder.currentRequestAttr>ibutes(RequestContextHolder.java:131)
at
org.springframework.web.context.request.AbstractRequestAttributesScope.get(Abst>ractRequestAttributesScope.java:42)
at
org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(Abstrac>tBeanFactory.java:350)
many suggestions on internet were to add listerner RequestContextListener. But problem persisted even if I added listener in webConfigurer.java in onStartup method.
{
servletContext.addListener(RequestContextListener.class);
}
But of no use.
Any leads would be appreciated.

I found a workaround for this. I don't know why TokenRelayRequestIntercepton isn't working but you can use your own RequestInterceptor based on Spring's SecurityContext.
First, define a RequestInterceptor :
public class MyRequestInterceptor implements RequestInterceptor {
public static final String AUTHORIZATION = "Authorization";
public static final String BEARER = "Bearer";
public MyRequestInterceptor() {
super();
}
#Override
public void apply(RequestTemplate template) {
// demander un token à keycloak et le joindre à la request
Optional<String> header = getAuthorizationHeader();
if (header.isPresent()) {
template.header(AUTHORIZATION, header.get());
}
}
public static Optional<String> getAuthorizationHeader() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.getDetails() != null && authentication.getDetails() instanceof OAuth2AuthenticationDetails) {
OAuth2AuthenticationDetails oAuth2AuthenticationDetails =
(OAuth2AuthenticationDetails) authentication.getDetails();
return Optional.of(String.format("%s %s", oAuth2AuthenticationDetails.getTokenType(),
oAuth2AuthenticationDetails.getTokenValue()));
} else {
return Optional.empty();
}
}
}
and then, declare a config class for your feign client using your RequestInterceptor, it should contains something like this :
#Bean(name = "myRequestInterceptor")
public RequestInterceptor getMyRequestInterceptor() throws IOException {
return new MyRequestInterceptor();
}
Your Feign client shoud look like this:
#FeignClient(name = "SERVICE_NAME", configuration = MyFeignConfiguration.class)
public interface MyRestClient {

I had the same issue with Feign Client running on startup using ApplicationRunner and I came up with following solution.
I defined my FeignClientsConfiguration with OAuth2FeignRequestInterceptor, which accepts predefined bean DefaultOAuth2ClientContext and OAuth2 configuration OAuth2ProtectedResourceDetails:
#Configuration
public class MyConfig extends FeignClientsConfiguration {
#Bean
public RequestInterceptor oauth2FeignRequestInterceptor( DefaultOAuth2ClientContext oAuth2ClientContext, MyOauth2Properties properties) {
return new OAuth2FeignRequestInterceptor(oAuth2ClientContext, resourceDetails(properties));
}
#Bean
public DefaultOAuth2ClientContext oAuth2ClientContext() {
return new DefaultOAuth2ClientContext();
}
private OAuth2ProtectedResourceDetails resourceDetails(MyOauth2Properties oauth2Properties) {
ResourceOwnerPasswordResourceDetails resourceDetails = new ResourceOwnerPasswordResourceDetails();
resourceDetails.setAccessTokenUri(oauth2Properties.getAccessTokenUri());
resourceDetails.setUsername(oauth2Properties.getUsername());
resourceDetails.setPassword(oauth2Properties.getPassword());
resourceDetails.setClientId(oauth2Properties.getClientId());
return resourceDetails;
}
}
Your feign client will look something like this:
#FeignClient(url = "http://localhost:8080/api/v1")
public interface FeignClient {
}
After all this, calling FeignClient from ApplicationRunner.run() works fine.
Spring Boot 2.2.6

Related

Retry feign client properites

I need to retry feign call for certain http status code and after 3 second for maximum 4 time.
Is there any properties that i can define on my application.yml or i need to write my custom Retryer that implement Retry interface
Feign has a build in Retryer however you can not configure the Retryer via application.yml. I guess the Spring Boot Team assumed that people would use the deprecated Hystrix project for this matter.
Instead of configuring Feign by config you could write a bit of code:
https://cloud.spring.io/spring-cloud-openfeign/reference/html/index.html#creating-feign-clients-manually
In addition you have to map the corresponding status code to RetryableException using a custom ErrorDecoder.
public class CustomErrorDecoder implements ErrorDecoder {
private final ErrorDecoder errorDecoder = new Default();
#Override
public Exception decode(String methodKey, Response response) {
Exception exception = defaultErrorDecoder.decode(s, response);
if(exception instanceof RetryableException){
return exception;
}
if(response.status() == 499){
return new RetryableException("499 blub", response.request().httpMethod(), null );
}
return exception;
}
}
public class Example {
public static void main(String[] args) {
MyApi myApi = Feign.builder()
.errorDecoder(new CustomErrorDecoder())
.target(MyApi.class, "https://api.hostname.com");
}
}
You can use retryable annotation.
Ex: You can throw custom exception when http status code is equal to 404
#Service
public interface MyService {
#Retryable(value = CustomException.class, maxAttempts = 2, backoff = #Backoff(delay = 100))
void retry(String str) throws CustomException;
}

Catching exception Feign

I want to handle any exception from feign client, even if service is not available. However I can not catch them using try/catch. This is my feign client:
#FeignClient(name = "api-service", url ="localhost:8888")
public interface ClientApi extends SomeApi {
}
Where api is:
#Path("/")
public interface SomeApi {
#GET
#Path("test")
String getValueFromApi();
}
Usage of client with try/catch:
#Slf4j
#Service
#AllArgsConstructor
public class SampleController implements SomeApi {
#Autowired
private final ClientApi clientApi;
#Override
public String getValueFromApi() {
try {
return clientApi.getValueFromApi();
} catch (Throwable e) {
log.error("CAN'T CATCH");
return "";
}
}
}
Dependencies are in versions:
spring-boot 2.2.2.RELEASE
spring-cloud Hoxton.SR1
Code should work according to How to manage Feign errors?.
I received few long stack traces among them exceptions are :
Caused by: java.net.ConnectException: Connection refused (Connection refused)
Caused by: feign.RetryableException: Connection refused (Connection refused) executing GET http://localhost:8888/test
Caused by: com.netflix.hystrix.exception.HystrixRuntimeException: ClientApi#getValueFromApi() failed and no fallback available.
How to properly catch Feign exeptions, even if client service (in this case localhost:8888) is not available?
Ps. When feign client service is available it works, ok. I am just focused on the exceptions aspect.
A better way to handle the situation where your service is not available is to use a circuit breaker pattern. Fortunately, it is easy using Netflix Hystrix as an implementation of the circuit breaker pattern.
First of all, you need to enable Hystrix for feign clients in application configuration.
application.yml
feign:
hystrix:
enabled: true
Then you should write a fallback class for the specified feign client interface.
In this case getValueFormApi method in fallback class will act mostly like catch block that you wrote(with exception when circuit will be in open state and original method will not be attempted).
#Component
public class ClientApiFallback implements ClientApi {
#Override
public String getValueFromApi(){
return "Catch from fallback";
}
}
Lastly, you just need to specify the fallback class for your feign client.
#FeignClient(name = "api-service", url ="localhost:8888", fallback = ClientApiFallback.class)
public interface ClientApi extends SomeApi {
}
That way your method getValueFromApi is fail safe. If,
for any reason, any uncaught exceptions escape from getValueFromApi the ClientApiFallback method will be called.
To enable circuit breaker and also configure your application to deal with unexpected errors, you need to:
1.- Enable the circuit breaker itself
#SpringBootApplication
#EnableFeignClients("com.perritotutorials.feign.client")
#EnableCircuitBreaker
public class FeignDemoClientApplication {
2.- Create your fallback bean
#Slf4j
#Component
#Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class PetAdoptionClientFallbackBean implements PetAdoptionClient {
#Setter
private Throwable cause;
#Override
public void savePet(#RequestBody Map<String, ?> pet) {
log.error("You are on fallback interface!!! - ERROR: {}", cause);
}
}
Some things you must keep in mind for fallback implementations:
Must be marked as #Component, they are unique across the application.
Fallback bean should have a Prototype scope because we want a new one to be created for each exception.
Use constructor injection for testing purposes.
3.- Your ErrorDecoder, to implement fallback startegies depending on the HTTP error returned:
public class MyErrorDecoder implements ErrorDecoder {
private final ErrorDecoder defaultErrorDecoder = new Default();
#Override
public Exception decode(String methodKey, Response response) {
if (response.status() >= 400 && response.status() <= 499) {
return new MyCustomBadRequestException();
}
if (response.status() >= 500) {
return new RetryableException();
}
return defaultErrorDecoder.decode(methodKey, response);
}
}
4.- In your configuration class, add the Retryer and the ErrorDecoder into the Spring context:
#Bean
public MyErrorDecoder myErrorDecoder() {
return new MyErrorDecoder();
}
#Bean
public Retryer retryer() {
return new Retryer.Default();
}
You can also add customization to the Retryer:
class CustomRetryer implements Retryer {
private final int maxAttempts;
private final long backoff;
int attempt;
public CustomRetryer() {
this(2000, 5); //5 times, each 2 seconds
}
public CustomRetryer(long backoff, int maxAttempts) {
this.backoff = backoff;
this.maxAttempts = maxAttempts;
this.attempt = 1;
}
public void continueOrPropagate(RetryableException e) {
if (attempt++ >= maxAttempts) {
throw e;
}
try {
Thread.sleep(backoff);
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
}
}
#Override
public Retryer clone() {
return new CustomRetryer(backoff, maxAttempts);
}
}
If you want to get a functional example about how to implement Feign in your application, read this article.

How to get request in MyBatis Interceptor

I want to measure time of sql execution which will be run by MyBatis (Spring Boot project) and bind that with other request parameters, so I can get full info about performance issues regarding specific requests. For that case I have used MyBatis Interceptor on following way:
#Intercepts({
#Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
#Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class QueryMetricsMybatisPlugin implements Interceptor {
#Override
public Object intercept(Invocation invocation) throws Throwable {
Stopwatch stopwatch = Stopwatch.createStarted();
Object result = invocation.proceed();
stopwatch.stop();
logExectionTime(stopwatch, (MappedStatement) invocation.getArgs()[0]);
return result;
}
}
Now when it come to binding with request, I want to store those metrics in request as attribute. I have tried this simple solution to get request, but that was not working since request was always null (I have read that this solution won't work in async methods, but with MyBatis Interceptor and its methods I think that's not the case):
#Autowired
private HttpServletRequest request;
So, the question is how properly get request within MyBatis interceptor?
One important note before I answer your question: it is a bad practice to access UI layer in the DAO layer. This creates dependency in the wrong direction. Outer layers of your application can access inner layers but in this case this is other way round. Instead of this you need to create a class that does not belong to any layer and will (or at least may) be used by all layers of the application. It can be named like MetricsHolder. Interceptor can store values to it, and in some other place where you planned to get metrics you can read from it (and use directly or store them into request if it is in UI layer and request is available there).
But now back to you question. Even if you create something like MetricsHolder you still will face the problem that you can't inject it into mybatis interceptor.
You can't just add a field with Autowired annotation to interceptor and expect it to be set. The reason for this is that interceptor is instantiated by mybatis and not by spring. So spring does not have chance to inject dependencies into interceptor.
One way to handle this is to delegate handling of the interception to a spring bean that will be part of the spring context and may access other beans there. The problem here is how to make that bean available in interceptor.
This can be done by storing a reference to such bean in the thread local variable. Here's example how to do that. First create a registry that will store the spring bean.
public class QueryInterceptorRegistry {
private static ThreadLocal<QueryInterceptor> queryInterceptor = new ThreadLocal<>();
public static QueryInterceptor getQueryInterceptor() {
return queryInterceptor.get();
}
public static void setQueryInterceptor(QueryInterceptor queryInterceptor) {
QueryInterceptorRegistry.queryInterceptor.set(queryInterceptor);
}
public static void clear() {
queryInterceptor.remove();
}
}
Query interceptor here is something like:
public interface QueryInterceptor {
Object interceptQuery(Invocation invocation) throws InvocationTargetException, IllegalAccessException;
}
Then you can create an interceptor that will delegate processing to spring bean:
#Intercepts({
#Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class }),
#Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}) })
public class QueryInterceptorPlugin implements Interceptor {
#Override
public Object intercept(Invocation invocation) throws Throwable {
QueryInterceptor interceptor = QueryInterceptorRegistry.getQueryInterceptor();
if (interceptor == null) {
return invocation.proceed();
} else {
return interceptor.interceptQuery(invocation);
}
}
#Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
#Override
public void setProperties(Properties properties) {
}
}
You need to create an implementation of the QueryInterceptor that does what you need and make it a spring bean (that's where you can access other spring bean including request which is a no-no as I wrote above):
#Component
public class MyInterceptorDelegate implements QueryInterceptor {
#Autowired
private SomeSpringManagedBean someBean;
#Override
public Object interceptQuery(Invocation invocation) throws InvocationTargetException, IllegalAccessException {
// do whatever you did in the mybatis interceptor here
// but with access to spring beans
}
}
Now the only problem is to set and cleanup the delegate in the registry.
I did this via aspect that was applied to my service layer methods (but you can do it manually or in spring mvc interceptor). My aspect looks like this:
#Aspect
public class SqlSessionCacheCleanerAspect {
#Autowired MyInterceptorDelegate myInterceptorDelegate;
#Around("some pointcut that describes service methods")
public Object applyInterceptorDelegate(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
QueryInterceptorRegistry.setQueryInterceptor(myInterceptorDelegate);
try {
return proceedingJoinPoint.proceed();
} finally {
QueryInterceptorRegistry.clear();
}
}
}

Hystrix and Spring #Async in combination

I'm using Hystrix library for the Spring Boot project (spring-cloud-starter-hystrix). I have a #Service class annotated with #HystrixCommand and it works as expected.
But, when I add the method annotated with #Async in that same service class then the Hystrix doesn't work, and fallback method is never called. What could cause this problem and how to fix it?
This is the Application class:
#EnableCircuitBreaker
#EnableHystrixDashboard
#SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
This is the service class:
#Service
public class TemplateService {
#HystrixCommand(
fallbackMethod = "getGreetingFallback",
commandProperties = {#HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1500")}
)
public String getGreeting() {
URI uri = URI.create("http://localhost:8090/greeting");
ResponseEntity<String> response = restTemplate.exchange(uri, HttpMethod.GET, null, String.class);
if (response.getStatusCode().equals(HttpStatus.OK)) {
return response.getBody();
} else {
return null;
}
}
public String getGreetingFallback(Throwable e) {
return null;
}
#Async
public void async(String message) {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
logger.info(MessageFormat.format("Received async message {0}", message));
}
}
#EnableAsync annotation is placed in a different class annotated with #Configuration, where I set some other Thread Executor options from properties file.
Given the code for TemplateService (which doesn't implement interface) and assuming the defaults on #EnableAsync it is safe to concur that CGLIB proxies are created by spring.
Thus the #HystrixCommand annotation on getGreeting() isn't inherited by the service proxy class; which explains the reported behavior.
To get past this error keep the #HystrixCommand and #Async method separated in different service because enabling JDK proxies will also not help and I am not sure about AspectJ mode.
Refer this for further information on Spring proxy mechanism.

Multiple servlet mappings in Spring Boot

Is there any way to set via property 'context-path' many mappings for a same Spring Boot MVC application? My goal is to avoid creating many 'Dispatcherservlet' for the uri mapping.
For example:
servlet.context-path =/, /context1, context2
You can create #Bean annotated method which returns ServletRegistrationBean , and add multiple mappings there. This is more preferable way, as Spring Boot encourage Java configuration rather than config files:
#Bean
public ServletRegistrationBean myServletRegistration()
{
String urlMapping1 = "/mySuperApp/service1/*";
String urlMapping2 = "/mySuperApp/service2/*";
ServletRegistrationBean registration = new ServletRegistrationBean(new MyBeautifulServlet(), urlMapping1, urlMapping2);
//registration.set... other properties may be here
return registration;
}
On application startup you'll be able to see in logs:
INFO | localhost | org.springframework.boot.web.servlet.ServletRegistrationBean | Mapping servlet: 'MyBeautifulServlet' to [/mySuperApp/service1/*, /mySuperApp/service2/*]
You only need a single Dispatcherservlet with a root context path set to what you want (could be / or mySuperApp).
By declaring multiple #RequestMaping, you will be able to serve different URI with the same DispatcherServlet.
Here is an example. Setting the DispatcherServlet to /mySuperApp with #RequestMapping("/service1") and #RequestMapping("/service2") would exposed the following endpoints :
/mySuperApp/service1
/mySuperApp/service2
Having multiple context for a single servlet is not part of the Servlet specification. A single servlet cannot serve from multiple context.
What you can do is map multiple values to your requesting mappings.
#RequestMapping({"/context1/service1}", {"/context2/service1}")
I don't see any other way around it.
You can use 'server.contextPath' property placeholder to set context path for the entire spring boot application. (e.g. server.contextPath=/live/path1)
Also, you can set class level context path that will be applied to all the methods e.g.:
#RestController
#RequestMapping(value = "/testResource", produces = MediaType.APPLICATION_JSON_VALUE)
public class TestResource{
#RequestMapping(method = RequestMethod.POST, value="/test", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<TestDto> save(#RequestBody TestDto testDto) {
...
With this structure, you can use /live/path1/testResource/test to execute save method.
None of the answers to this sort of question seem to mention that you'd normally solve this problem by configuring a reverse proxy in front of the application (eg nginx/apache httpd) to rewrite the request.
However if you must do it in the application then this method works (with Spring Boot 2.6.2 at least) : https://www.broadleafcommerce.com/blog/configuring-a-dynamic-context-path-in-spring-boot.
It describes creating a filter, putting it early in the filter chain and basically re-writing the URL (like a reverse proxy might) so that requests all go to the same place (ie the actual servlet.context-path).
I've found an alternative to using a filter described in https://www.broadleafcommerce.com/blog/configuring-a-dynamic-context-path-in-spring-boot that requires less code.
This uses RewriteValve (https://tomcat.apache.org/tomcat-9.0-doc/rewrite.html) to rewrite urls outside of the context path e.g. if the real context path is "context1" then it will map /context2/* to /context1/*
#Component
public class LegacyUrlWebServerFactoryCustomizer implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
private static final List<String> LEGACY_PATHS = List.of("context2", "context3");
#Override
public void customize(TomcatServletWebServerFactory factory) {
RewriteValve rewrite = new RewriteValve() {
#Override
protected void initInternal() throws LifecycleException {
super.initInternal();
try {
String config = LEGACY_PATHS.stream() //
.map(p -> String.format("RewriteRule ^/%s(/.*)$ %s$1", p, factory.getContextPath())) //
.collect(Collectors.joining("\n"));
setConfiguration(config);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
};
factory.addEngineValves(rewrite);
}
}
If you need to use HTTP redirects instead then there is a little bit more required (to avoid a NullPointerException in sendRedirect):
#Component
public class LegacyUrlWebServerFactoryCustomizer implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
private static final List<String> LEGACY_PATHS = List.of("context2", "context3");
#Override
public void customize(TomcatServletWebServerFactory factory) {
RewriteValve rewrite = new RewriteValve() {
#Override
protected void initInternal() throws LifecycleException {
super.initInternal();
try {
String config = LEGACY_PATHS.stream() //
.map(p -> String.format("RewriteRule ^/%s(/.*)$ %s$1 R=permanent", p, factory.getContextPath())) //
.collect(Collectors.joining("\n"));
setConfiguration(config);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
#Override
public void invoke(Request request, Response response) throws IOException, ServletException {
if (request.getContext() == null) {
String[] s = request.getRequestURI().split("/");
if (s.length > 1 && LEGACY_PATHS.contains(s[1])) {
request.getMappingData().context = new FailedContext();
}
}
super.invoke(request, response);
}
};
factory.addEngineValves(rewrite);
}
}
I use this approach:
import javax.servlet.ServletContext;
import javax.servlet.ServletRegistration;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
#Configuration
public class WebAppInitializer implements WebApplicationInitializer {
#Override
public void onStartup(ServletContext servletContext) {
AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext();
rootContext.register(AppConfig.class);
rootContext.setServletContext(servletContext);
ServletRegistration.Dynamic dispatcher = servletContext.addServlet("dispatcher", new DispatcherServlet(rootContext));
dispatcher.setLoadOnStartup(1);
dispatcher.addMapping("/mapping1/*");
dispatcher.addMapping("/mapping2/*");
servletContext.addListener(new ContextLoaderListener(rootContext));
}
}

Resources