For our dreitier Hub application I wanted to provide a subdomain for each of our customers. In addition to that, I wanted to use some other subdomains for static and dynamic content. At that moment I did explicitly not wanted to deal with microservices or multiple applications.

When receiving a request, Spring should do the following:

  • Is the subdomain one of our known subdomains and mapped to a @Controller annotated class? If yes, use anntoated given controller.
  • Belongs the subdomain to one of our tenants and is mapped to a @Controller annotated class? If yes, use the given controller.
  • In any other case, use any @Controller annotated class which can handle the requested path. The subdomain does not matter.

If you don’t care about the details, you can directly purchase a complete demo project on Gumroad:

Sample Spring Boot project for subdomain handling with Spring Web MVC

Researching the topic

Spring Boot / Spring MVC is not able to deal with subdomains so I had to dig into how to make it work.

I have not been the first one with this kind of requirement. But not many resources are out there and up-to-date. If you are searching for this topic, you should definitely look at 

Basically, you have a custom annotation like @SubdomainController and some kind of implementation of org.springframework.web.servlet.mvc.condition.RequestCondition.
Both are tied together with a custom implementation of org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping.

All of the links above don’t describe how you can have mulitple RequestConditions to deal with both known subdomains and dynamically resolved subdomains for tenants.

Creating annotations for subdomain and tenant controllers

In my case I needed two annotations: @SubdomainController and @TenantController.
Both annotations are straight forward. In my case I just wanted them to be applied only on the class level but not on methods. A class annotated with @SubdomainController should handle multiple subdomains.

	@Controller
	@RequestMapping
	@Target({ ElementType.TYPE })
	@Retention(RetentionPolicy.RUNTIME)
	public @interface SubdomainController {
		String[] value();
	}

For example, you should be able to access either http://www.${your-domain} or http://test.${your-domain} with the following class definition:

	@SubdomainController(value = { "www", "test" })
	public class PublicSubdomainController {
		@RequestMapping(value = { "", "/" })
		public ResponseEntity<?> index() {
			return ResponseEntity.ok("Hello from www.${your-domain} or test.${your-domain}");
		}
	}

@TenantController should be applicable to any class and handle requests to a tenant subdomain.

	
	@Controller
	@RequestMapping
	@Target({ ElementType.TYPE })
	@Retention(RetentionPolicy.RUNTIME)
	public @interface TenantController {
	}

I wanted to use something like this

	@TenantController
	public class TenantController {
		@RequestMapping("/")
		public ResponseEntity<?> index() {
			return ResponseEntity.ok(String.format("Hello from tenant %s", TenantContext.getTenant().getName()));
		}
	}

RequestCondition checks if a request mapping is considered for an HTTP request

After reading the links above, I understand that both annotations needed a belonging RequestCondition implementation.
RequestCondition is called for each HttpServletRequest and checks if a given conditions matches. Not more, not less.

	public class SubdomainRequestCondition implements RequestCondition<SubdomainRequestCondition> {
		@Override
		public SubdomainRequestCondition getMatchingCondition(HttpServletRequest request) {
			SubdomainRequestCondition r = null;

			// you would check for the assigned subdomains
			if (request.getServerName().startsWith("mydomain")) {
				return this;
			}

			return null;
		}

		@Override
		public int compareTo(SubdomainRequestCondition other, HttpServletRequest request) {
			return 0;
		}
	
		@Override
		public SubdomainRequestCondition combine(SubdomainRequestCondition other) {
			// not relevant at the moment
			return null;
		}
	}

It took me a while to understand the following important facts about a RequestCondition:

  1. It can be attached to any bean during the initialization the Spring Boot application.
  2. It does not know anything about the controller nor the annotation it has been currently attached to.
  3. During an HTTP request, Spring checks the assigned RequestCondition -if any- for each registered @RequestMapping. If RequestCondition.getMatchingCondition() returns its own instance (not null), the annotated controller is considered to be used for the current request.
  4. There can only be one RequestCondition assigned to a controller instance. If you need to check multiple RequestConditions you can use org.springframework.web.servlet.mvc.condition.CompositeRequestCondition.

Checking for a tenant subdomain with TenantRequestCondition

For classes annotated with @TenantController we use a custom RequestConditionTenantRequestCondition performs a lookup against a TenantRepository. It contains all tenants with their belonging subdomains:

	public class TenantRequestCondition implements RequestCondition<TenantRequestCondition> {
		// ...
		@Override
		public TenantRequestCondition getMatchingCondition(HttpServletRequest request) {
			String subdomain = SubdomainUtil.extractSubdomain(request);
			TenantRequestCondition r = null;

			Optional<Tenant> tenant = tenantRepository.findBySubdomain(subdomain);

			// ... set tenant context etc.

			if (tenant.isPresent()) {
				return this;
			}

			return null;
		}
	}

If the tenant’s subdomain is known, the controller is used. ThreadLocal is used for storing the current tenant in the request.

Assigning RequestConditions to Spring MVC controllers

During application start-up an instance of the class RequestMappingHandlerMapping is used for resolving the request mappings. For each registered handler (e.g. a controller instance class or method) having a @RequestMapping annotated, getCustomTypeCondition() or getCustomMethodCondition() is called.

To register our SubdomainRequestCondition and TenantRequestCondition above, we have to provide our own RequestMappingHandlerMapping instance:

	public SubdomainRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
		@Autowired
		TenantRepository tenantRepository;

		@Override
		protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {
			// if the given class is annotated with SubdomainController, we prefer it
			SubdomainController subdomainController = null;
	
			if ((subdomainController = AnnotationUtils.findAnnotation(handlerType, SubdomainController.class)) != null) {
				// we need to extract the mapped subdomains as we don't have access to this
				// information during runtime
				return new SubdomainRequestCondition(new HashSet<>(Arrays.asList(subdomainController.value())));
			}
	
			// ... do something with the TenantController ...
	
			// no SubdomainController or TenantController annotation? Then don't use any
			// conditions and fallback to the default handling without any condiitions.
			return null;
		}
	}

The getCustomTypeCondition() method is only called if a @RequestMapping is applied on class level. It checks if the class has a @SubdomainController annotation. Due to my @SubdomainController definition above, this is always the case.
If the @SubdomainController annotation is present, we assign the class our new RequestCondition. We have to pass the assigned subdomains during start-up. As already mentioned before, the RequestCondition does not know anything about the controller in the current HTTP request context.

Registering the RequestMappingHandlerMapping

The last step was to register our SubdomainRequestMappingHandlerMapping inside Spring MVC. You can only have one instance of RequestMappingHandlerMapping in your application. The configuration interface WebMvcRegistrations allows us to overwrite the default RequestMappingHandlerMapping instance and replace it with our own:

	@Component
	public class CustomWebMvcRegistrations implements WebMvcRegistrations {
	
		@Override
		public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
			return new SubdomainRequestMappingHandlerMapping();
		}
	}

What I have achieved

The code above solves my requirements to have a single Spring Boot web application dealing with multiple subdomains:

  • I can have subdomains like microsite1.mydomain.com and microsite2.mydomain.com which can be handled by the same Spring Boot web applications.
  • Additionally I can have subdomains like tenant1.mydomain.com and tenant2.mydomain.com. These subdomains can be looked up in a database and be dynamically generated.
  • I can still use @Controller annotations to fallback to any unknown subdomain.

If you need the whole demo project, you can purchase it on Gumroad:

Sample Spring Boot project for subdomain handling with Spring Web MVC

I am asking you for a donation.

You liked the content or this article has helped and reduced the amount of time you have struggled with this issue? Please donate a few bucks so I can keep going with solving challenges.