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

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() { }
}

Related

How to register DispatcherServlets and override onStartup when extending SpringBootServletInitializer

I have a spring boot application that is being deployed to an external instance of Tomcat. The main class extends SpringBootServletInitializer and overrides the methods configure and onStartup. In the method configure, a custom environment is initialized to provide an encryptor for decrypting user passwords for activemq because jasypt is not initialized until after activemq. This I got from the jasypt-spring-boot documentation.
#Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
try {
StandardEncryptableServletEnvironment env = StandardEncryptableServletEnvironment.builder().encryptor(encryptor).build();
return application.environment(env).sources(AxleServer.class);
} catch (FileNotFoundException e) {
logger.error("Could not load encryptable environment", e);
return application.sources(AxleServer.class);
}
}
In the method onStartup I am configuring DispatcherServlets:
#Override
public void onStartup(ServletContext servletContext) throws ServletException {
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(RemotingConfig.class);
servletContext.addListener(new ContextLoaderListener(context));
ServletRegistration.Dynamic restDispatcher = servletContext.addServlet("rest-dispatcher", new DispatcherServlet(context));
restDispatcher.setLoadOnStartup(1);
restDispatcher.addMapping("/rest/*");
ServletRegistration.Dynamic remotingDispatcher = servletContext.addServlet("remoting-dispatcher", new DispatcherServlet(context));
remotingDispatcher.setLoadOnStartup(2);
remotingDispatcher.addMapping("/remoting/*");
}
The problem is that the configure method is never called because it is called from the super implementation of onStartup. I tried calling the super implementation super.onStartup(servletContext); but then I get an error that a root context already exists when deploying the app.
What is the correct way to register the DispatcherServlets and to override the onStartup method? Is it required to call the super implementation of onStartup?
I was able to solve this issue by first calling the super.onStartup and then getting the root context from ServletContext attributes
#Override
public void onStartup(ServletContext servletContext) throws ServletException {
super.onStartup(servletContext);
Object obj = servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
if(obj instanceof AnnotationConfigServletWebServerApplicationContext) {
AnnotationConfigServletWebServerApplicationContext context = (AnnotationConfigServletWebServerApplicationContext) obj;
context.register(RemotingConfig.class);
ServletRegistration.Dynamic restDispatcher = servletContext.addServlet("rest-dispatcher", new DispatcherServlet(context));
restDispatcher.setLoadOnStartup(1);
restDispatcher.addMapping("/rest/*");
ServletRegistration.Dynamic remotingDispatcher = servletContext.addServlet("remoting-dispatcher", new DispatcherServlet(context));
remotingDispatcher.setLoadOnStartup(2);
remotingDispatcher.addMapping("/remoting/*");
}
}

How to register the Spring MVC dispatcher servlet with an embedded tomcat without spring-boot?

My question is similar to this one Embedded Tomcat Integrated With Spring. I want to run a Spring MVC Dispatcher Servlet on an embedded Tomcat. But I always end up with an exception saying that the WebApplicationObjectSupport instance does not run within a ServletContext.
My example has just the two classes:
class Application {
public static void main(String[] args) throws LifecycleException, ServletException {
try (AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext()) {
context.registerShutdownHook();
context.register(WebConfig.class);
context.refresh();
Tomcat tomcat = new Tomcat();
tomcat.setPort(9090);
File base = new File("");
System.out.println(base.getAbsolutePath());
Context rootCtx = tomcat.addWebapp("", base.getAbsolutePath());
DispatcherServlet dispatcher = new DispatcherServlet(context);
Tomcat.addServlet(rootCtx, "SpringMVC", dispatcher);
rootCtx.addServletMapping("/*", "SpringMVC");
tomcat.start();
tomcat.getServer().await();
}
}
}
#Configuration
#EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
#Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**").addResourceLocations("classpath:/assets/");
}
#Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("redirect:index.html");
}
}
How do I have to define the servlet context differently then by calling the tomcat.addWebApp(..) method? Does anybody have an example how the Spring MVC dispatcher can be used with an embedded tomcat but without boot?
You can create a ServletContextInitializer and sneak it in via #Configuration:
#Configuration
class WebAppInitConfig implements ServletContextInitializer {
#Override
void onStartup(ServletContext context) {
AnnotationConfigWebApplicationContext webAppContext = new AnnotationConfigWebApplicationContext()
webAppContext.register(RootConfig)
webAppContext.registerShutdownHook()
ServletRegistration.Dynamic dispatcher = context.addServlet("dispatcher", new DispatcherServlet(webAppContext))
dispatcher.loadOnStartup = 1
dispatcher.addMapping("/*")
// Whatever else you need
}
}

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 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!

Spring application can't reach jsp pages

In my spring application, I using the configuration based on java code instead xml files. When I try access any page from my application, the browser is redirect to the correct mapped url, but I still face a 404 error page.
My controller class is created this way:
#Controller
#RequestMapping(value="acesso")
public class AcessoController {
#RequestMapping(value="login")
public ModelAndView login() {
ModelAndView mav = new ModelAndView();
mav.setViewName("acesso/login");
return mav;
}
}
My WebAppInitializer.java is this:
#Order(value=1)
public class WebAppInitializer implements WebApplicationInitializer {
#SuppressWarnings("resource")
#Override
public void onStartup(ServletContext container) {
// Create the 'root' Spring application context
AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext();
rootContext.register(WebAppConfig.class);
// Create the dispatcher servlet's Spring application context
AnnotationConfigWebApplicationContext jspContext = new AnnotationConfigWebApplicationContext();
jspContext.register(JspDispatcherConfig.class);
// Register and map the dispatcher servlet
ServletRegistration.Dynamic jsp_dispatcher = container.addServlet("jsp_dispatcher", new DispatcherServlet(jspContext));
jsp_dispatcher.setLoadOnStartup(1);
jsp_dispatcher.addMapping("*.jsp");
// Create the dispatcher servlet's Spring application context
AnnotationConfigWebApplicationContext jsonContext = new AnnotationConfigWebApplicationContext();
jsonContext.register(JsonDispatcherConfig.class);
// Register and map the dispatcher servlet
ServletRegistration.Dynamic json_dispatcher = container.addServlet("json_dispatcher", new DispatcherServlet(jsonContext));
json_dispatcher.setLoadOnStartup(2);
json_dispatcher.addMapping("*.json");
// Create the dispatcher servlet's Spring application context
AnnotationConfigWebApplicationContext xmlContext = new AnnotationConfigWebApplicationContext();
xmlContext.register(XmlDispatcherConfig.class);
// Register and map the dispatcher servlet
ServletRegistration.Dynamic xml_dispatcher = container.addServlet("xml_dispatcher", new DispatcherServlet(jspContext));
xml_dispatcher.setLoadOnStartup(3);
xml_dispatcher.addMapping("*.xml");
}
}
My jspDispatcherConfig.java is this:
#Configuration
#Import(WebAppConfig.class)
public class JspDispatcherConfig {
#Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setPrefix("/WEB-INF/view/jsp/");
viewResolver.setSuffix(".jsp");
return viewResolver;
}
}
In my folder WEB-INF, I have the following structure:
-view
--json
--jsp
---acesso
----login.jsp
---erro
----publico
----privado
---privado
----admin.jsp
----customer.jsp
--xml
Anyone can tell me what I am doing wrong here?
OK, then I solve this problem modifying the mapping string in the controllers: instead of use, by example,
#RequestMapping(value="login")
I use this:
#RequestMapping(value="login.htm")
(or other extension, depending of type of the view - jsp, json or xml). Now it's working fine.

Resources