Returning a different Tomcat session ID based on URI in Spring - spring

I'm very new to Spring + Tomcat and trying to learn it while working on an existing spring boot (v. 2.6.x) web app running on a Tomcat server (tomcat-embed-core v. 9.0.x).
The application serves URLs of this type:
\{customer}\path\to\resource
\{customer}\newapp\path\to\resource
I can see this reported on the ModelAndView controller annotated with the likes of PostMapping and GetMapping.
Now, the service keeps tracks of sessions using JSESSION cookies and generating session IDs using tomcat's standard org.apache.catalina.SessionIdGenerator.
I would like to programmatically change the format of the session identifier generated by tomcat, depending on the request being served.
Given the paths above, for instance, I'd like to have:
request to 1. above, should be generating session identifiers like 905A6892CB2C12F84A331F58A6A2C382
requests to newapp, i.e. 2. above, should be generating session identifiers like NEWAPP_905A6892CB2C12F84A331F58A6A2C382
The format of the session is irrelevant, but they must have a different prefix.
It seems that one way to achieve this would be to have two different contexts, each one with a different catalina Manager, which can be set using org.apache.catalina.Context#setManager.
I'm not able to define two different contexts because the root in the path is the variable {customer}, nor I'm able to dynamically inject the context using an implementation of org.apache.catalina.valves.ValveBase, as there is no easy way to create a delegated context, where the only difference WRT the base tomcat context is the Manager.
I've tried changing the root of the newapp paths to be like newapp\{customer}\path\to\resource and creating a new Context for newapp, but this fails if I try to override org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory#getWebServer like
#Override
public WebServer getWebServer(ServletContextInitializer... initializers) {
Tomcat tomcat = new Tomcat();
File baseDir = createTempDir("tomcat");
tomcat.setBaseDir(baseDir.getAbsolutePath());
prepareContext(tomcat.getHost(), initializers);
final String documentRoot = getValidDocumentRoot().getAbsolutePath();
final Context ctx = tomcat.addContext("/newapp", documentRoot);
ctx.setManager(new NewAppManager());
return getTomcatWebServer(tomcat);
}
returning a 404 error - it looks like the application is not able to find the mapping for the path, even if I successfully changed the GetMapping to newapp\{customer}\path\to\resource.
Can anyone suggest the best way to achieve this and if what I'm trying even makes sense?
Thank you so much!

I was able to achieve the required outcome by adding a few lines in the getWebServer method.
Adding the whole method below, with comments, for clarity.
#Override
public WebServer getWebServer(ServletContextInitializer... initializers) {
Tomcat tomcat = new Tomcat();
File baseDir = createTempDir("tomcat");
tomcat.setBaseDir(baseDir.getAbsolutePath());
customizeEngine(tomcat.getEngine());
prepareContext(tomcat.getHost(), initializers);
// This is the ROOT context generated by the embedded tomcat
final StandardContext rootCtx = (StandardContext) tomcat.getHost().findChild("");
// Both context will be sharing the same war file
final String documentRoot = getValidDocumentRoot().getAbsolutePath();
// Creating a new context for the "newapp, serving the "/newapp" path
final StandardContext newappCtx = (StandardContext) tomcat.addContext("/newapp", documentRoot);
// This turned out to be very important, sets the classloader of the context to delegate to the application classloader, exactly as ROOT ctx does
newappCtx.setParentClassLoader(rootCtx.getParentClassLoader());
newappCtx.setDelegate(true);
// This was needed in our specific case, the default cookie path would be "/newapp"
newappCtx.setSessionCookiePath("/");
// And finally setting the session manager, generating a new session identifier
newappCtx.setManager(new NewappSessionManager());
final WebServer ws = getTomcatWebServer(tomcat);
// This is the default dispatcher created by the embedded tomcat
final Servlet dispatcherServlet = ((StandardWrapper) rootCtx.findChild("dispatcherServlet")).getServlet();
// We're now adding the default servlet to the "newappCtx"
Tomcat.addServlet(newappCtx, "dispatcherServlet", dispatcherServlet);
// it will serve all paths under "/newapp"
newappCtx.addServletMappingDecoded("/*", "dispatcherServlet");
return ws;
}
....
// Simple manager for the new context
private static final class NewappSessionManager extends StandardManager {
#Override
protected String getNextSessionId() {
return "NEWAPP_" + super.getNextSessionId();
}
}

Related

Spring sleuth Baggage key not getting propagated

I've a filter (OncePerRequestFilter) which basically intercepts incoming request and logs traceId, spanId etc. which works well,
this filter lies in a common module which is included in other projects to avoid including spring sleuth dependency in all of my micro-services, the reason why I've created it as a library because any changes to library will be common to all modules.
Now I've to add a new propagation key which need to be propagated to all services via http headers like trace and spanId for that I've extracted current span from HttpTracing and added a baggage key to it (as shown below)
Span span = httpTracing.tracing().tracer().currentSpan();
String corelationId =
StringUtils.isEmpty(request.getHeader(CORELATION_ID))
? "n/a"
: request.getHeader(CORELATION_ID);
ExtraFieldPropagation.set(CUSTOM_TRACE_ID_MDC_KEY_NAME, corelationId);
span.annotate("baggage_set");
span.tag(CUSTOM_TRACE_ID_MDC_KEY_NAME, corelationId);
I've added propagation-keys and whitelisted-mdc-keys to my application.yml (with my library) file like below
spring:
sleuth:
propagation-keys:
- x-corelationId
log:
slf4j:
whitelisted-mdc-keys:
- x-corelationId
After making this change in filter the corelationId is not available when I make a http call to another service with same app, basically keys are not getting propagated.
In your library you can implement ApplicationEnvironmentPreparedEvent listener and add the configuration you need there
Ex:
#Component
public class CustomApplicationListener implements ApplicationListener<ApplicationEvent> {
private static final Logger log = LoggerFactory.getLogger(LagortaApplicationListener.class);
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ApplicationEnvironmentPreparedEvent) {
log.debug("Custom ApplicationEnvironmentPreparedEvent Listener");
ApplicationEnvironmentPreparedEvent envEvent = (ApplicationEnvironmentPreparedEvent) event;
ConfigurableEnvironment env = envEvent.getEnvironment();
Properties props = new Properties();
props.put("spring.sleuth.propagation-keys", "x-corelationId");
props.put("log.slf4j.whitelisted-mdc-keys:", "x-corelationId");
env.getPropertySources().addFirst(new PropertiesPropertySource("custom", props));
}
}
}
Then in your microservice you will register this custom listener
public static void main(String[] args) {
ConfigurableApplicationContext context = new SpringApplicationBuilder(MyApplication.class)
.listeners(new CustomApplicationListener()).run();
}
I've gone through documentation and seems like I need to add spring.sleuth.propagation-keys and whitelist them by using spring.sleuth.log.slf4j.whitelisted-mdc-keys
Yes you need to do this
is there another way to add these properties in common module so that I do not need to include them in each and every micro services.
Yes, you can use Spring Cloud Config server and a properties file called application.yml / application.properties that would set those properties for all microservices
The answer from Mahmoud works great when you want register the whitelisted-mdc-keys programatically.
An extra tip when you need these properties also in a test, then you can find the anwser in this post: How to register a ApplicationEnvironmentPreparedEvent in Spring Test

Using a custom classloader with Spring's ComponentScan and PropertyPlaceholderConfigurer

I want to create multiple application contexts in my Tomcat application.
Some of these application contexts have the same package and class names, but they all refer to different jars.
For example:
application0 use service.jar, model.jar
application1 use service-a.jar, model-a.jar
application2 use service-b.jar, model-b.jar
application0 context is OK because is in orign project.
I reference some web page to custom application1, I use my custom classloader to start applicationContext.
File file0 = new File("D://git/project1/service-a.jar");
File file1 = new File("D://git/project1/modele-a.jar");
// convert the file to URL format
URL url0 = file0.toURI().toURL();
URL url1 = file1.toURI().toURL();
List<URL> urls = new LinkedList<>();
List<File> libs = listFilesForFolder(new File("D://protal//apache-tomcat-8.0.39//lib"));
for(File lib : libs) {
urls.add(lib.toURI().toURL());
}
urls.add(url1);
urls.add(url0);
final URLClassLoader customClassLoader = new URLClassLoader(urls.toArray(new URL[urls.size()]));
ClassPathXmlApplicationContext context1 = new ClassPathXmlApplicationContext("applicationContext.xml") {
protected void initBeanDefinitionReader(XmlBeanDefinitionReader reader)
{
super.initBeanDefinitionReader(reader);
reader.setValidationMode(XmlBeanDefinitionReader.VALIDATION_NONE);
reader.setBeanClassLoader(customClassLoader);
}
};
allApplicationContexts.add(context1);
The Spring contexts start OK, but they fail to create the component-scan bean, and PropertyPlaceholderConfigurer isn't working. Everything else seems correct.
I sure my config is correct because it works without the custom classloader. Libs contains all spring lib.
Is it possible to get this working with multiple Spring contexts?

Behavior differ between running application in IntelliJ and from jar

I have written an application using Jetty as a server. I wish to package my application into a .jar, so I may put it in a Docker container later on.
When I run my application through IntelliJ, everything seems to be working as it should, and I can reach my index.html from localhost:2222. However, when I package my application (using mvn clean compile assembly:single) and run my jar by typing java -jar myapplication.jar in the shell, my index.html on localhost:2222 returns HTTP Error 404.
My App.java:
public class App {
public static void main( String[] args ) throws Exception {
ResourceConfig config = new ResourceConfig();
config.packages("mypackage");
ServletHolder servlet = new ServletHolder(new ServletContainer(config));
Server server = new Server(2222);
ResourceHandler handler = new ResourceHandler();
handler.setBaseResource(org.eclipse.jetty.util.resource.Resource
.newClassPathResource("index.html"));
ServletContextHandler context = new ServletContextHandler(server, "/*");
context.addServlet(servlet, "/*");
HandlerList handlers = new HandlerList();
handlers.addHandler(handler);
handlers.addHandler(context);
server.setHandler(handlers);
try {
server.start();
server.join();
} finally {
server.destroy();
}
}
}
When running the application in IntelliJ and as jar, I get the following output (may be of interest?):
2015-11-06 09:25:00.991:INFO::main: Logging initialized #279ms
2015-11-06 09:25:01.238:WARN:oejsh.ContextHandler:main: o.e.j.s.ServletContextHandler#490d6c15{/,null,null} contextPath ends with /*
2015-11-06 09:25:01.238:WARN:oejsh.ContextHandler:main: Empty contextPath
2015-11-06 09:25:01.250:INFO:oejs.Server:main: jetty-9.2.3.v20140905
Nov 06, 2015 9:25:01 AM org.glassfish.jersey.server.ApplicationHandler initialize
INFO: Initiating Jersey application, version Jersey: 2.7 2014-03-12 18:11:31...
2015-11-06 09:25:02.294:INFO:oejsh.ContextHandler:main: Started o.e.j.s.ServletContextHandler#490d6c15{/,null,AVAILABLE}
2015-11-06 09:25:02.306:INFO:oejs.ServerConnector:main: Started ServerConnector#268f106e{HTTP/1.1}{0.0.0.0:2222}
2015-11-06 09:25:02.306:INFO:oejs.Server:main: Started #1688ms
I am not posting my file of Jersey-methods, since I don't think it is relevant for my question.
So, what is the magic of packaging this up to a jar that can recognize my index.html at localhost:2222? Is it some environment settings in IntelliJ?
Cheers
First of all, don't mix ResourceHandler and ServletContextHandler, you are duplicating static content effort (3 times over with Jersey in play) and will get unreliable results.
Drop the ResourceHandler, set the required ServletContextHandler.setBaseResource(), and add a DefaultServlet to your context.
// Figure out what path to serve content from
ClassLoader cl = App.class.getClassLoader();
// We look for a file, as ClassLoader.getResource() is not
// designed to look for directories (we resolve the directory later)
URL f = cl.getResource("static-root/index.html");
if (f == null)
{
throw new RuntimeException("Unable to find resource directory");
}
// Resolve file to directory
URI webRootUri = f.toURI().resolve("./").normalize();
System.err.println("Main Base Resource is " + webRootUri);
// Setup the basic application "context" for this application at "/"
// This is also known as the handler tree (in jetty speak)
ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
context.setContextPath("/");
context.setBaseResource(Resource.newResource(webRootUri));
server.setHandler(context);
// Jersey setup
ResourceConfig config = new ResourceConfig();
config.packages("mypackage");
context.addServlet(new ServletHolder(new ServletContainer(config)), "/*");
// Lastly, the default servlet for root content (always needed, to satisfy servlet spec)
// It is important that this is last.
ServletHolder holderDef = new ServletHolder("default",DefaultServlet.class);
holderDef.setInitParameter("dirAllowed","true");
context.addServlet(holderDef,"/");
Note: since you setup Jersey at the url-pattern /* you should know that Jersey is responsible for serving all static content, not Jetty.

How are IntelliJ 13, Tomcat 7 (Servlet 3), Spring 4 and context/servlet mappings related

If you take a empty IntelliJ 13 project setup for Spring 4 and Tomcat, configured entirely with Java (no XML) you could initialize your dispatcher servelet and Tomcat with something like this:
1
public class WebAppInit extends AbstractAnnotationConfigDispatcherServletInitializer
{
#Override
protected Class<?>[] getRootConfigClasses()
{
return new Class<?>[]{SpringConfig.class};
}
#Override
protected Class<?>[] getServletConfigClasses()
{
return new Class<?>[]{SpringWebMVCConfig.class};
}
#Override
protected String[] getServletMappings()
{
return new String[]{"/url"};
}
#Override
protected Filter[] getServletFilters()
{
CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter();
characterEncodingFilter.setEncoding("UTF-8");
return new Filter[]{characterEncodingFilter};
}
}
2
Then there is this in Run Configuration in IntelliJ13:
3
Finally there is the mapping of the rest controller:
#RestController
public class RootController
{
#Autowired
private SomeDAO someDAO;
#RequestMapping(value = "/")
public String root()
{
return someDAO.getStuff();
}
}
The question I have is what controls what?
A) If I change the mapping in 1 the request mappings (3) no longer work.
B) If I set 1 and 3 to "/" and change 2 to .../url then I hit the controller with .../url in the browser.
C) But if I set 1 and 2 to .../url2 then I get 404 when I go to .../url2 in the browser.
I always thought that 1 sets the mapping for the Spring dispatcher servlet, 2 sets the app context as far as the IDE is concerned (similarly to if you named your war file "url" and then dropped into webapps) and 3 is just a url mapping relative to the servlet context. If that's the case then I don't understand why case C results in 404.
In IntelliJ, you're configuring the context path of the web application to /url2. That means that all the requests to URLs starting by /url2/ are routed by Tomcat to a component of the webapp, whereas all the other requests are routed by Tomcat to another webapp (or result in a 404 if no webapp is mapped to the URL)
In the Spring webapp configuration, you're configuring the path, inside the webapp, to which the Spring servlet is mapped. And you're configuring it to /url2. So, when you remove the context path from the URL (which is used to choose the webapp), the resulting path is then inspected and, if it is /url2, then it goes to the Spring servlet. Othewise, it goes to another resource in the webapp.
The end result is that, to hit the Spring servlet, you need a path like
http://localhost:8080/url2/url2
Note that mapping the Spring servlet to a path like /url2 doesn't make much sense, because only this path would lead to the servlet, which is supposed to be used for many different paths, to which controllers are mapped. You probably want to mapp it to /url2/* which would mean that all the requests for a path, within the webapp (so after the context path), starting with /url2/, would go to the Spring servlet.

Embedded Jetty to test the spring application startup

What I am trying to do is, to have a test to startup the whole application to see if there's any error. But I want to use the applicationContext.xml from the /src/resource folder and not form test/resource. How can I do that in JUnit?
My application is big and a lot of people share the same codebase. So, I just wanted to have a quick test to see if the checkin can start up the application.
This is my simple code but it looks like it's missing some of the autowire stuff, that's why I want to use the xml files from /src/resource, so I don't have to maintain two locations.
My application is plain Spring MVC 3.0
#Test(enabled = false)
public void shouldStartupTheApp() throws Exception {
Server server = new Server();
SelectChannelConnector connector = new SelectChannelConnector();
connector.setPort(9999);
server.setConnectors(new Connector[] {connector});
Context context = new Context(server, "/", Context.SESSIONS);
DispatcherServlet dispatcherServlet = new DispatcherServlet();
dispatcherServlet.setContextConfigLocation("classpath:/test-applicationContext.xml");
ServletHolder servletHolder = new ServletHolder(dispatcherServlet);
context.addServlet(servletHolder, "/*");
server.start();
}
You can import your src/resources/filename.xml in other xml-file using
<import resource="classpath:/filename.xml" />

Resources