Java configuration of SimpleUrlHandlerMapping (Spring boot) - spring

I have an existing Spring web application, that uses two controllers, which extend AbstractController. I want to integrate Spring Boot into the application, so that we can run it as a standalone application.
I am facing a problem, because Spring is not forwarding the calls to my controller. How can I map the controller to an URL pattern like "/app/*"?
SampleController.java
#Controller("myController")
public class SampleController extends AbstractController {
#Override
protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
response.getWriter().print("Hello world!");
return null;
}
}
Application.java
#EnableAutoConfiguration
#Configuration
#ComponentScan
public class Application {
public static void main(String[] args) throws Exception {
SpringApplication.run(Application.class, args);
}
#Bean
public SimpleUrlHandlerMapping sampleServletMapping() {
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
Properties urlProperties = new Properties();
urlProperties.put("/index", "myController");
mapping.setMappings(urlProperties);
return mapping;
}
}
When I start the application I get the following message:
INFO [org.springframework.web.servlet.handler.SimpleUrlHandlerMapping] Mapped URL path [/index] onto handler 'myController'
But when I send a request to /index, I get following messages:
DEBUG [org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping] Looking up handler method for path /index
DEBUG [org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping] Did not find handler method for [/index]
DEBUG [org.springframework.web.servlet.handler.SimpleUrlHandlerMapping] Matching patterns for request [/index] are [/**]
DEBUG [org.springframework.web.servlet.handler.SimpleUrlHandlerMapping] URI Template variables for request [/index] are {}
DEBUG [org.springframework.web.servlet.handler.SimpleUrlHandlerMapping] Mapping [/index] to HandlerExecutionChain with handler [org.springframework.web.servlet.resource.ResourceHttpRequestHandler#11195d3e] and 1 interceptor

SimpleUrlHandlerMappings are ordered and, as described in the javadoc the default is Integer.MAX_VALUE which means that they have the lowest possible precedence. This causes ResourceHttpRequestHandler (which is mapped to /** and has an order of Integer.MAX_VALUE - 1 by default) to take precedence over the mapping for your controller.
Update your sampleServletMapping() method to set your mapping's order to a value that's less than Integer.MAX_VALUE - 1. For example:
#Bean
public SimpleUrlHandlerMapping sampleServletMapping() {
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
mapping.setOrder(Integer.MAX_VALUE - 2);
Properties urlProperties = new Properties();
urlProperties.put("/index", "myController");
mapping.setMappings(urlProperties);
return mapping;
}

Related

Spring doesn't add interceptors to SimpleUrlHandlerMapping

I am adding inceprtors by exposing a bean in a following way:
#Bean
public WebMvcConfigurer webMvcConfigurer() {
return new WebMvcConfigurer() {
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(cacheClearanceInterceptor()).addPathPatterns("/*")
.order(OFFSET + 1);
registry.addInterceptor(pageNotFoundInterceptor()).addPathPatterns("/*")
.excludePathPatterns("/static/*").order(OFFSET + 2);
}
};
}
I am using SimpleUrlHandlerMapping for resolving url to handler methods. However, I don't see above interceptors added. I was expecting spring will add interceptors but it's not and request hits to handler method directly without interceptors in the middle. Do I need to call setInteceptors method manually to register inceptors on SimpleUrlHandlerMapping ?

SpringMVC, using two contexts and two dispatchers for Rest Service and normal contents

In my SpringMVC project configuration, I have a RootContextConfiguration and other two context configuration files for rest services and normal requests namely; RestServletContextConfiguration and WebServletContextConfiguration.
And, I'm bootstrapping the application as in the following code.
public class Bootstrap implements WebApplicationInitializer
{
#Override
public void onStartup(ServletContext container) throws ServletException
{
container.getServletRegistration("default").addMapping("/resource/*");
AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext();
rootContext.register(RootContextConfiguration.class);
container.addListener(new ContextLoaderListener(rootContext));
AnnotationConfigWebApplicationContext restContext = new AnnotationConfigWebApplicationContext();
restContext.register(RestServletContextConfiguration.class);
DispatcherServlet restServlet = new DispatcherServlet(restContext);
restServlet.setDispatchOptionsRequest(true);
ServletRegistration.Dynamic springRestDispatcher = container.addServlet("springRestDispatcher", restServlet);
springRestDispatcher.setLoadOnStartup(1);
springRestDispatcher.addMapping("/api/*");
AnnotationConfigWebApplicationContext webContext = new AnnotationConfigWebApplicationContext();
webContext.register(WebServletContextConfiguration.class);
DispatcherServlet webServlet = new DispatcherServlet(webContext);
ServletRegistration.Dynamic springWebDispatcher = container.addServlet("springWebDispatcher", webServlet );
springWebDispatcher.setLoadOnStartup(2);
springWebDispatcher.setMultipartConfig(new MultipartConfigElement(null, 20_971_520L, 41_943_040L, 512_000));
springWebDispatcher.addMapping("/*");
}
}
I need /api/cars to resolve to
#RestController
#RequestMapping("/cars")
class CarRestController{}
And /cars to resolve to
#Controller
#RequestMapping("/cars")
class CarController{}
However, deployment fails because of ambiguous mapping. If I change the mapping of CarRestController to #RequestMapping('/api/cars') then I can access that controller with the path /api/api/cars (Note the double api prefix). But what I want is to be able to access the CarRestController with /api/cars.
What should I do to achieve my goal?. Highly appreciate your help.
Why you dont put mapping #RequestMapping on your method ?
For example
#RestController
public class YourClass {
#RequestMapping("/car")
public String yourMethod() { }
}

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));
}
}

Spring Boot - access component inside a servlet

I have a spring boot application that serves my jersey based api. I have a requirement to have the services layer serve blob data to a client as a stream. I wrote a servlet to do that and configured it as follows.
#Bean
public ServletRegistrationBean servletRegistrationBean(){
return new ServletRegistrationBean(new BlobReaderServlet(),"/blobReader/*");
}
However, in the servlet code I can't seem to inject any components (they are all null). I need to inject a component that actually loads the blob data from the database.
#WebServlet(name = "BlobReaderServlet",
urlPatterns = {"/blobreader"})
#Component
public class BlobReaderServlet extends HttpServlet {
Logger logger = Logger.getLogger(this.getClass().getName());
#Inject
DocumentLoaderComponent blobLoader;
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
I strongly suspect the servlet isn't a spring managed component after all and dependency injection isn't working. How can I get access to a component from the context?
UPDATE
It was much simpler than I thought.
#Override
public void init() throws ServletException {
ApplicationContext ac = (ApplicationContext) getServletConfig().getServletContext().getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
this.documentBlob = (DocumentBlob) ac.getBean("documentBlobBean");
}
You are correct that your servlet isn't a Spring managed bean. That is because you are instantiating the instance directly (i.e., calling new BlobReaderServlet() in your bean method). Another solution is to change your configuration class as follows:
#Bean
public ServletRegistrationBean servletRegistrationBean(){
return new ServletRegistrationBean(blobReaderServlet(),"/blobReader/*");
}
#Bean
public BlobReaderServlet blobReaderServlet(){
return new BlobReaderServlet();
}
This will allow Spring to manage the instance and perform autowiring on it.

Spring Boot with multiple DispatcherServlet, each having their own #Controllers

Basically I want to split my application into 2 parts. Each part has it's own security stuff and own #Controllers. The #Services should be accessible from both parts.
So I thought, I should get 2 DispatcherServlet. One listening to /admin/* and the second listening to everything else ( / ). Each of those will have its own AnnotationConfigWebApplicationContext so I can have separate component scan for the #Controllers.
And because Spring Boot provides one DispatcherServlet listening on / out of the box, I thought, I can just add a second one:
#Configuration
public class MyConfig {
#Bean(name="myDS")
public DispatcherServlet myDS(ApplicationContext applicationContext) {
AnnotationConfigWebApplicationContext webContext = new AnnotationConfigWebApplicationContext();
webContext.setParent(applicationContext);
webContext.register(MyConfig2.class);
// webContext.refresh();
return new DispatcherServlet(webContext);
}
#Bean
public ServletRegistrationBean mySRB(#Qualifier("myDS") DispatcherServlet dispatcherServlet) {
ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(dispatcherServlet);
servletRegistrationBean.addUrlMappings("/admin/*");
servletRegistrationBean.setName("adminServlet");
return servletRegistrationBean;
}
}
The MyConfig2 class, only has #Configuration and #ComponentScan. Within the same package is a #Controller.
When starting the application, I can see, that the second servlet mapping is getting registered, but the #Controller is not. Additionally I can now access all #Controllers from / and /admin.
Any idea how I can get this working?
I got it working somehow!
Here's my Package Layout:
test.foo.
FooConfig.java
FooController.java
test.bar.
BarConfig.java
BarController.java
test.app.
Application.java
MyService.java
src/main/resources/application.properties
Application.java:
#SpringBootApplication(exclude=DispatcherServletAutoConfiguration.class)
public class Application {
public static void main(String[] args) throws Exception {
SpringApplication.run(Application.class, args);
}
#Bean
public ServletRegistrationBean foo() {
DispatcherServlet dispatcherServlet = new DispatcherServlet();
AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
applicationContext.register(FooConfig.class);
dispatcherServlet.setApplicationContext(applicationContext);
ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(dispatcherServlet, "/foo/*");
servletRegistrationBean.setName("foo");
return servletRegistrationBean;
}
#Bean
public ServletRegistrationBean bar() {
DispatcherServlet dispatcherServlet = new DispatcherServlet();
AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
applicationContext.register(BarConfig.class);
dispatcherServlet.setApplicationContext(applicationContext);
ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(dispatcherServlet, "/bar/*");
servletRegistrationBean.setName("bar");
return servletRegistrationBean;
}
}
The exclude does prevent Spring Boot from creating its own DispatcherServlet with / mapping. You can remove that line, if you want that mapping or define your own.
You can add servletRegistrationBean.setLoadOnStartup(1) if you want to have your Servlets initialized on application start. Else it will wait for the first request for that servlet.
It's important to set servletRegistrationBean.setName(...), else the servlets will override each other.
FooConfig.java & BarConfig.java:
#Configuration #ComponentScan #EnableWebMvc
public class FooConfig { }
#EnableWebMvc will enable the component scan. Without it, it won't find the #Controller class.
The Controller and Service code is not important. You just have to know, that if you have #RequestMapping("/foo") inside FooController, the request must be GET /foo/foo because the Servlet's URL mapping is /foo/*. It's not possible to call the URL GET /foo because the Servlet URL mapping needs a / at the end of its path (in other words: GET /foo will look for a Servlet with / mapping!), though #RequestMapping("") must be called via GET /foo/. And of course it was not possible to use /foo or /foo* as Servlet mapping (or I just did not find the correct settings for that)
Scope: The Controllers can't see each other, though it's not possible to #Autowired them in each other. Also the Service can't #Autowired any of the Controllers. But the Controllers can #Autowired the Service.
Though it's a classical parent child context hierarchy.
The only "bad" thing is, that we need #EnableMvcConfig and don't get the auto configured sugar from Spring boot within the context. The parent context is getting auto configured. I put some database stuff within the application.properties and did a query inside MyService which got called by FooController and it worked flawlessly! :)
I hope this may help some people!

Resources