I have Spring Boot application with REST API mapped on /api. I need to define additional servlet on /. I want all request that match /api was handled by REST API and all others requests by the servlet. How to do this?
#SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
#RestController
#RequestMapping("/api")
public class ApiController {
#GetMapping
public String get() {
return "api";
}
}
#Bean
public ServletRegistrationBean customServletBean() {
return new ServletRegistrationBean<>(new HttpServlet() {
#Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.getWriter().println("custom");
}
}, "/*");
}
}
In code above I want something like this:
curl http://localhost:8080/api/
> api⏎
curl http://localhost:8080/custom/
> custom
I have tried with filter to redirect requests, but all requests go to custom servlet:
#Bean
public FilterRegistrationBean apiResolverFilter() {
final FilterRegistrationBean registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter((req, response, chain) -> {
HttpServletRequest request = (HttpServletRequest) req;
String path = request.getRequestURI().substring(request.getContextPath().length());
if (path.startsWith("/api/")) {
request.getRequestDispatcher(path).forward(request, response);
} else {
chain.doFilter(request, response);
}
});
registrationBean.addUrlPatterns("/*");
return registrationBean;
}
This project is available on github: https://github.com/mariuszs/nestedweb
When mapping a servlet to the root path you will override the mapping for the DispatcherServlet which, by default, is mapped to /.
There are basically 3 solutions you could try
Map the DispatcherServlet to /api and modify the mappings in your controllers
Use a ServletForwardingController to forward the request to the configured but unmapped Servlet
Use a ServletWrappingController to wrap a Servlet instance
Number 2 and 3 are almost the same, with this difference that with option 3 Spring also manages the Servlet instance whereas with option 2, the Servlet container manages the Servlet.
Mapping DispatcherServlet to /api
Option 1 can be an option if all of your controllers are mapped under /api, if they aren't this isn't an option. In your application.properties you would set the spring.mvc.servlet.path to /api. Then you would configure your other Servlet like you did in your question.
Use a ServletForwardingController
Spring provides a ServletForwardingController which will lookup a Servlet in the ServletContext given the name of the servlet and forward the request to it. You will still have to register the Servlet but prevent it from being mapped.
Next you would need a SimpleUrlHandlerMapping to map the URLs to this controller or set it as the default handler (basically a catch all).
#Bean
public ServletForwardingController forwarder() {
ServletForwardingController controller = new ServletForwardingController();
controller.setServletName("my-servlet");
return controller;
}
#Bean
public CustomServlet customServlet() {
return new CustomServlet();
}
#Bean
public ServletRegistrationBean customServletRegistration() {
ServletRegistrationBean registration = new ServletRegistrationBean(customServlet(), false);
registration.setServletName("customServlet");
return registration;
}
#Bean
public SimpleUrlHandlerMapping simpleUrlHandlerMapping() {
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
mapping.setDefaultHandler(forwarder());
mapping.setOrder(LOWEST_PRECEDENCE - 2);
return mapping;
}
Use a ServletWrappingController
Spring provides a ServletWrappingController which will internally create and configure a Servlet instance. It acts as an adapter from/to the Servlet to a Spring Controller. You don't have to register the CustomServlet in this case and is thus slightly easier to configure the then ServletForwardingController.
Next you would need a SimpleUrlHandlerMapping to map the URLs to this controller or set it as the default handler (basically a catch all).
#Bean
public ServletWrappingController wrapper() {
ServletWrappingController controller = new ServletWrappingController ();
controller.setServletName("my-servlet");
controller.setServletClass(CustomerServlet.class);
return controller;
}
#Bean
public SimpleUrlHandlerMapping simpleUrlHandlerMapping() {
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
mapping.setDefaultHandler(wrapper());
mapping.setOrder(LOWEST_PRECEDENCE - 2);
return mapping;
}
Depending on your architecture and url structure you might want to go for option 1 or option 3.
Related
I am trying to inject a auth service to a Filter -
#Autowired
AuthRequestService authService;
And use it in doFiler method -
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
System.out.println("SAPServiceFilter: "+ req.getRequestURI());
//TODO - create auth sender
authService.isAuthnticate((HttpServletRequest)request); //null
chain.doFilter(request, response);
}
My filter class in sub-package of my #SpringBootApplication class and annotated with #service -
#Service
public class AuthRequestService {
#PostConstruct
public void init() {
System.out.println("AuthRequestService #PostConstruct");
}
public boolean isAuthnticate(HttpServletRequest request) {
System.out.println("isAuthnticate");
return true;
}
}
The class also appears when listing all my beans using -
for (String name : applicationContext.getBeanDefinitionNames()) {
System.out.println(name);
}
Still when debugging authService is null, one last thing the filter is registered with FilterRegistrationBean -
FilterRegistrationBean<SAPServiceFilter> filterRegBean = new FilterRegistrationBean<>();
filterRegBean.setFilter(new SAPServiceFilter());
You could use constructor injection. Supposed your filter registration bean lives in a component and has access to the service you could autowire it there and pass it with the constructor
#Autowired
AuthRequestService authRequestService;
[...]
FilterRegistrationBean<SAPServiceFilter> filterRegBean = new FilterRegistrationBean<>();
filterRegBean.setFilter(new SAPServiceFilter(authRequestService));
Your filter is not under the control of spring. That´s why the autowired dependencies are not being injected.
In your filter init code add this line:
SpringBeanAutowiringSupport.processInjectionBasedOnCurrentContext(this);
or
SpringBeanAutowiringSupport.processInjectionBasedOnServletContext(this,getServletContext());
But there are more other ways to register a servlet filter in spring context:
https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-embedded-container-servlets-filters-listeners-beans
I have a function with #GetMapping(value = "/getToken") that writes json content.
#GetMapping(value = "/getToken")
public String getToken(HttpServletRequest request, HttpServletResponse response, Model model) {
// jsonObject
PrintWriter out = response.getWriter();
out.print(jsonObject);
}
Now, A user can make a GET Request to above mapping using a url like this:
localhost:8080/getToken?username="username"&password="password"
I have also created a class called CORSFilter that implements javax.servlet.Filter and i want this filter to intercept only those request that have /getToken in the request path.
#WebFilter(urlPatterns = "/getToken")
public class CORSFilter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
//validate user and password
chain.doFilter(requestToUse, responseToUse);
}
}
Now, when I hit localhost:8080 or localhost:8080/thankyou in the browser then the above filter is getting called.
How can I stop this? I want to call above filter only if the url path is localhost:8080/getToken?username="user"&password="pass"
Unfortunately, Spring Boot doesn't honor the urlPatterns for filters declared with WebFilter. If you need to apply a pattern, you'll have to declare the filter as a bean and declare a filter registration for that bean, in a configuration class. For example:
#Bean
public FilterRegistrationBean<CORSFilter> corsFilterRegistration() {
FilterRegistrationBean<CORSFilter> filterRegistrationBean =
new FilterRegistrationBean<>(corsFilter());
filterRegistrationBean.setUrlPatterns(Collections.singleton("/getToken"));
return filterRegistrationBean;
}
#Bean
public CORSFilter corsFilter() {
return new CORSFilter();
}
Note however that Spring Boot has native support for CORS. You don't need any filter for that.
You can use interceptors- HandlerInterceptor. Please refer below url for how-to.
https://www.journaldev.com/2676/spring-mvc-interceptor-example-handlerinterceptor-handlerinterceptoradapter
If it is pure jsp based application then you need to declare it in web.xml because #WebFilter doesn't guarantee the order of execution of filters or if it is spring based application then you can use #CrossOrigin on method level or declare a bean to filter each and every url as mentioned in the below url
Spring CORS
I would like to display custom 404 error page if a user enters a request path that isn't handled by any controller.
Suppose that I have only one controller and it handles the following request paths: path1, path2 and path3. How can I display a custom 404 error page if a user enters path4? Currently, Tomcat displays its own 404 error page. I would like to use annotations and not implement this task by configuring any xml files.
My current approach doesn't achieve this requirement.It might be possible to configure any filters, but I am not aware of such approach. Thus, I will appreciate any ideas about it.
Here is my code:
public class WebAppContextInitializer implements WebApplicationInitializer {
#Override
public void onStartup(ServletContext servletContext) throws ServletException {
AnnotationConfigWebApplicationContext annotationConfigWebApplicationContext = new AnnotationConfigWebApplicationContext();
annotationConfigWebApplicationContext.register(WebContextConfiguration.class);
DispatcherServlet dispatcherServlet = new DispatcherServlet(annotationConfigWebApplicationContext);
dispatcherServlet.setThrowExceptionIfNoHandlerFound(true);
ServletRegistration.Dynamic dispatcher = servletContext.addServlet("SpringDispatcher",dispatcherServlet);
dispatcher.setLoadOnStartup(1);
dispatcher.addMapping("/");
servletContext.setInitParameter("spring.profiles.active","demo");
}
}
#ControllerAdvice
class GlobalControllerExceptionHandler {
#ExceptionHandler(NoHandlerFoundException.class)
public String handleConflict() {
return "404_error_page";
}
}
I am currently using Spring MVC 4.0.5 and would like to use Spring Web Flow for some process oriented pages. However, I think there is still some problem with my configuration.
In the server logs:
2014-09-15 20:54:49,280 [localhost-startStop-1] DEBUG org.springframework.webflow.definition.registry.FlowDefinitionRegistryImpl - Registering flow definition 'ServletContext resource [/WEB-INF/flows/registration/registration-flow.xml]' under id 'registration'
However, when accessing it, the log says
2014-09-15 20:54:49,820 [http-bio-8080-exec-2] DEBUG org.springframework.webflow.mvc.servlet.FlowHandlerMapping - No flow mapping found for request with URI '/appContext/registration/'
Here is my configuration for the web flow
#Configuration
public class WebFlowConfig extends AbstractFlowConfiguration {
private Logger logger = Logger.getLogger(WebFlowConfig.class);
#Bean
#Autowired
public FlowExecutor flowExecutor(FlowDefinitionRegistry flowRegistry,
PlatformTransactionManager txManager, SessionFactory sessionFactory) {
return getFlowExecutorBuilder(flowRegistry)
.addFlowExecutionListener(new SecurityFlowExecutionListener(),
"*")
.addFlowExecutionListener(
new HibernateFlowExecutionListener(sessionFactory,
txManager), "*").build();
}
#Bean
#Autowired
public FlowDefinitionRegistry flowRegistry(
FlowBuilderServices flowBuilderServices) {
return getFlowDefinitionRegistryBuilder(flowBuilderServices)
.setBasePath("/WEB-INF/flows")
.addFlowLocationPattern("/**/*-flow.xml").build();
}
#Bean
#Autowired
public FlowBuilderServices flowBuilderServices(
MvcViewFactoryCreator mvcViewFactoryCreator, Validator validator) {
return getFlowBuilderServicesBuilder()
.setViewFactoryCreator(mvcViewFactoryCreator)
.setValidator(validator).setDevelopmentMode(true).build();
}
#Bean
#Autowired
public MvcViewFactoryCreator mvcViewFactoryCreator(
InternalResourceViewResolver viewResolver) {
MvcViewFactoryCreator factoryCreator = new MvcViewFactoryCreator();
factoryCreator.setViewResolvers(Arrays.asList(viewResolver));
return factoryCreator;
}
#Bean
#Autowired
public FlowHandlerMapping flowHandlerMapping(FlowDefinitionRegistry registry) {
FlowHandlerMapping handlerMapping = new FlowHandlerMapping();
handlerMapping.setOrder(-1);
handlerMapping.setFlowRegistry(registry);
return handlerMapping;
}
#Bean
#Autowired
public FlowHandlerAdapter flowHandlerAdapter(FlowExecutor executor) {
FlowHandlerAdapter handlerAdapter = new FlowHandlerAdapter();
handlerAdapter.setFlowExecutor(executor);
handlerAdapter.setSaveOutputToFlashScopeOnRedirect(true);
return handlerAdapter;
}
}
Hope that someone can help. Thanks.
You are specifying FlowHandlerMapping in webflow config:
Implementation of HandlerMapping that follows a simple convention
for creating URL path mappings from the ids of registered flow definitions. This
implementation returns a FlowHandler that invokes a flow if the current request
path matches the id of a flow in the configured FlowDefinitionRegistry.
The default FlowUrlHandler implementation for Spring Web Flow is DefaultFlowUrlHandler.
Expects URLs to launch flow to be of this pattern:
http://<host>/[app context path]/[app servlet path]/<flow path>
The flow id is treated as the path info component of the request URL string.
If the path info is null, the servletPath will be used as the flow id. Also, if
the servlet path ends in an extension it will be stripped when calculating the flow id.
The flow definition URL for the given flow id will be built by appending the
flow id to the base app context and servlet paths.
No flow mapping found for request with URI '/appContext/registration/'
Your path info must have been null and servlet mapping is something like: /appContext/registration/* resulting in flow id as /appContext/registration/ which is not registered.
So check your servlet mapping.
Remove MvcViewFactoryCreator Definition And .setViewFactoryCreator(mvcViewFactoryCreator)
I have a single-page web app that's using Backbone.js client routing with pushState. In order to get this to work, I have to tell my server (Java, Spring 3, Tomcat) which URLs should be resolved on the server (actual JSP views, API requets), and which should simply be sent to the index page to be handled by the client. Currently I'm using an InternalResourceViewResolver to simply serve JSP views that match the name of the URL request. Since client-side URLs don't have a view on the server, the server returns a 404.
What is the best way to specify to Spring (or Tomcat) that a few specific URLs (my client-side routes) should all resolve to index.jsp, and anything else should fall through to the InternalResourceViewResolver?
I found that Spring MVC 3 added a tag that does exactly what I need, the mvc:view-controller tag. This got it done for me:
<mvc:view-controller path="/" view-name="index" />
<mvc:view-controller path="/admin" view-name="index" />
<mvc:view-controller path="/volume" view-name="index" />
http://static.springsource.org/spring/docs/3.0.x/reference/mvc.html
In theory, to handle navigation via history.pushState you want to return index.html for unhandled resources. If you look at official documentation for modern web frameworks it's often realised based on 404 status.
In spring you should handle resources in order:
path mapped REST controllers
app static resources
index.html for others
To do this you have at least 4 possible solutions.
Using EmbeddedServletContainerCustomizer and custom 404 handler
#Controller
static class SpaController {
#RequestMapping("resourceNotFound")
public String handle() {
return "forward:/index.html";
}
}
#Bean
public EmbeddedServletContainerCustomizer containerCustomizer() {
return container -> container.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND, "/resourceNotFound"));
}
Using custom default request mapping handler
#Autowired
private RequestMappingHandlerMapping requestMappingHandlerMapping;
static class SpaWithHistoryPushStateHandler {
}
static class SpaWithHistoryPushStateHandlerAdapter implements HandlerAdapter {
#Override
public boolean supports(final Object handler) {
return handler instanceof SpaWithHistoryPushStateHandler;
}
#Override
public ModelAndView handle(final HttpServletRequest request, final HttpServletResponse response, final Object handler) throws Exception {
response.getOutputStream().println("default index.html");
return null;
}
#Override
public long getLastModified(final HttpServletRequest request, final Object handler) {
return -1;
}
}
#Bean
public SpaWithHistoryPushStateHandlerAdapter spaWithHistoryPushStateHandlerAdapter() {
return new SpaWithHistoryPushStateHandlerAdapter();
}
#PostConstruct
public void setupDefaultHandler() {
requestMappingHandlerMapping.setDefaultHandler(new SpaWithHistoryPushStateHandler());
}
Using custom ResourceResolver
#Autowired
private ResourceProperties resourceProperties;
#Override
public void addResourceHandlers(final ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**")
.addResourceLocations(resourceProperties.getStaticLocations())
.setCachePeriod(resourceProperties.getCachePeriod())
.resourceChain(resourceProperties.getChain().isCache())
.addResolver(new PathResourceResolver() {
#Override
public Resource resolveResource(final HttpServletRequest request, final String requestPath, final List<? extends Resource> locations, final ResourceResolverChain chain) {
final Resource resource = super.resolveResource(request, requestPath, locations, chain);
if (resource != null) {
return resource;
} else {
return super.resolveResource(request, "/index.html", locations, chain);
}
}
});
}
Using custom ErrorViewResolver
#Bean
public ErrorViewResolver customErrorViewResolver() {
final ModelAndView redirectToIndexHtml = new ModelAndView("forward:/index.html", Collections.emptyMap(), HttpStatus.OK);
return (request, status, model) -> status == HttpStatus.NOT_FOUND ? redirectToIndexHtml : null;
}
Summary
Fourth option looks simplest but as always it depends what you need. You may also want to restric returning index.html only when request expects text/html (which BasicErrorController already do based on "produces" header).
I hope one of this options will help in your case.
I would give a clear scheme to my urls and separate frontend from backend.
Some suggestions:
Route all requests starting by /server to the backend and all others to the frontend.
Setup two different domains, one for the backend, one for the frontend.