Andrunevchyn

Andrunevchyn


October 2017
M T W T F S S
 1
2345678
9101112131415
16171819202122
23242526272829
3031  

Categories


Spring 5 + WebFlux without SpringBoot

Andriy AndrunevchynAndriy Andrunevchyn

I don’t like SpringBoot because of hidden dependencies and unknown springconfigs behind annotations. But there is one more problem with SpringBoot – as usual latest examples from Pivotal (and others as well) are written based on SpringBoot and sometimes it’s not possible easily to figure out how properly to set up plain Spring config. I’m running new project based on WebFlux and it should be old school war file which we can deploy on any webserver. So task is to configure WebFlux without SpringBoot. There are two ways  – mapping based on annotation which could be done even based on WebMvcConfigurer like this

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.context.annotation.PropertySources;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.JstlView;



@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "com.mydomain")

@PropertySources({ @PropertySource(value = "classpath:/properties/common.properties", ignoreResourceNotFound = true),
		@PropertySource(value = "classpath:/properties/dbConfig.properties") })
public class AppConfig extends WebMvcConfigurerAdapter {



	@Bean
	public ViewResolver viewResolver() {
		InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
		viewResolver.setViewClass(JstlView.class);
		viewResolver.setPrefix("/views/");
		viewResolver.setSuffix(".jsp");

		return viewResolver;
	}

	@Override
	public void addViewControllers(ViewControllerRegistry registry) {
		registry.addViewController("/testpage").setViewName("testpage");
	}
}
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration;

import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;

public class WebXmlConfig implements WebApplicationInitializer {
 
    public void onStartup(ServletContext container) throws ServletException {
 
        AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext();
        ctx.register(AppConfig.class);
        ctx.setServletContext(container);
 
        ServletRegistration.Dynamic servlet = container.addServlet("dispatcher", new DispatcherServlet(ctx));
 
        servlet.setLoadOnStartup(1);
        servlet.addMapping("/");
        
       
        
    }
   
}

or second approach – mapping based on routes. Unfortunately there is no clear explanation how to do it without SpringBoot. I found only mention about such kind of config in book “Spring 5 Recipes” so I took  it and added like

import javax.servlet.ServletContext;
import javax.servlet.ServletRegistration;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.http.server.reactive.ServletHttpHandlerAdapter;
import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;

public class WebXmlConfig implements WebApplicationInitializer {

	public void onStartup(ServletContext servletContext) {
		AnnotationConfigApplicationContext fluxContext = new AnnotationConfigApplicationContext(WebFluxConfig.class);

		HttpHandler httpHandler = WebHttpHandlerBuilder.applicationContext(fluxContext).build();
		ServletHttpHandlerAdapter handlerAdapter = new ServletHttpHandlerAdapter(httpHandler);
		ServletRegistration.Dynamic fluxServlet = servletContext.addServlet("SpringFluxDispatcher", handlerAdapter);

		fluxServlet.setLoadOnStartup(1);
		fluxServlet.setAsyncSupported(true);
		fluxServlet.addMapping("/");
	}

}

And guess what? It didn’t work for me! I spend some time on investigation and figured out my mapping is not correct I configured route like

import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.web.reactive.config.EnableWebFlux;
import org.springframework.web.reactive.config.WebFluxConfigurer;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;

import com.domain.testproject.EchoHandler;


@Configuration
@EnableWebFlux
@ComponentScan(basePackages = "com.domain.testproject", excludeFilters = { @Filter(Configuration.class) })
public class WebFluxConfig implements WebFluxConfigurer {
	
	@Bean
	public RouterFunction<ServerResponse> monoRouterFunction(EchoHandler echoHandler) {
		return route(GET("/echo"), echoHandler::echo);
	}

}

 

But the proper path actually should include war file name because I deployed war file and tomcat run project in folder with the same name so that the proper route is

return route(GET("/testproject/echo"), echoHandler::echo);

As usual Spring has many way to configure the same thing so you  can also write this route like this

return route(GET("/*/echo"), echoHandler::echo);

Also you could do like this

return route(request -> request.path().contains("/echo"), echoHandler::echo);

but it’s not really good idea cause urls http://host:port/testproject/echo and http://host:port/testproject/path/echo will be managed by the same handler

Actually I don’t understand why it was written like that. Why do we need project name on path? If we check class RequestPatternPredicate which we use for matching we will see the following

private static class PathPatternPredicate implements RequestPredicate {

		private final PathPattern pattern;

		public PathPatternPredicate(PathPattern pattern) {
			Assert.notNull(pattern, "'pattern' must not be null");
			this.pattern = pattern;
		}

		@Override
		public boolean test(ServerRequest request) {
			PathContainer pathContainer = request.pathContainer();
			PathPattern.PathMatchInfo info = this.pattern.matchAndExtract(pathContainer);
			traceMatch("Pattern", this.pattern.getPatternString(), request.path(), info != null);
			if (info != null) {
				mergeTemplateVariables(request, info.getUriVariables());
				return true;
			}
			else {
				return false;
			}
		}

Issue happens because in method matchAndExtract we pass as parameter container with the full path instead relative path. I don’t see other way except extending this class and overriding method test (( If I do it I’ll publish code later

The funny thing is  – plain annotation config works fine with the second approach either, no need to configure path with project name.

andriy@andrunevchyn.com

Comments 0
There are currently no comments.