62

An Alternative Approach to ThreadLocal Using Spring

 5 years ago
source link: https://www.tuicool.com/articles/hit/neMzI3J
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

An Alternative Approach to ThreadLocal Using Spring

DZone's Guide to

An Alternative Approach to ThreadLocal Using Spring

Want to learn more about this alternative approach to using ThreadLocal? Click here to find out more about using ThreadLocal in the Spring Framework.

Sep. 21, 18 · Java Zone ·

Free Resource

Join the DZone community and get the full member experience.

Bring content to any platform with the open-source BloomReach CMS. Try for free.

In a blog post published some time ago, Multitenant Applications Using Spring Boot JPA Hibernate and Postgres , I included code set to and to retrieve from the tenant identifier, a discriminator for selecting its associated data source using a  ThreadLocal   reference:

  • A context holder class for holding the tenant data:
...
public class DvdRentalTenantContext {

  private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();

  public static void setTenantId(String tenantId) {
    CONTEXT.set(tenantId);
  }

  public static String getTenantId() {
    return CONTEXT.get();
  }

  public static void clear() {
    CONTEXT.remove();
  }
}
  • A Spring MVC interceptor, which could have been done using a servlet filter, to set and clear the tenant identifier:
public class DvdRentalMultiTenantInterceptor extends HandlerInterceptorAdapter {

  private static final String TENANT_HEADER_NAME = "X-TENANT-ID";

  @Override
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    String tenantId = request.getHeader(TENANT_HEADER_NAME);
    DvdRentalTenantContext.setTenantId(tenantId);
    return true;
  }
...
}
  • And somewhere in the application:
String currentTenantId = DvdRentalTenantContext.getTenantId();

I normally try to avoid using ThreadLocal and would also advise to limit their usage. They can be handy, solve difficult problems but can also introduce memory leaks.

In this post, I will discuss how to use Spring’s ThreadLocalTargetSource   to prevent dealing directly with the dangerous ThreadLocal , while practicing dependency injection and proper mocking in unit tests.

Note:This approach would still use a  ThreadLocal   reference, but it delegates setting, injecting, and clearing it to the Spring framework.

Requirements

  • Java 7 or 8
  • Maven 3.3 or better
  • Familiarity with the Spring Framework

Create the Demo App

curl "https://start.spring.io/starter.tgz"
 -d dependencies=actuator,web
 -d language=java
 -d type=maven-project
 -d baseDir=threadlocaltargetsource-demo
 -d groupId=com.asimio
 -d artifactId=threadlocaltargetsource-demo
 -d version=0-SNAPSHOT
 | tar -xzvf -

This command will create a Maven project in a folder named threadlocaltargetsource-demo  with the actuator and web-related Spring Boot dependencies. Read on if you are interested in adding Spring Boot support using the bom approach .

Let’s take a look at the relevant classes included in this demo application: TenantStore.java.

package com.asimio.demo.tenant;
...
public class TenantStore {

  private String tenantId;

  public void clear() {
    this.tenantId = null;
  }

  // Getters / Setters
...
}

This class serves as the tenant data holder.

AppConfig.java :

package com.asimio.demo.config;
...
@Configuration
public class AppConfig {

  @Bean
  public Filter tenantFilter() {
    return new TenantFilter();
  }

  @Bean
  public FilterRegistrationBean tenantFilterRegistration() {
    FilterRegistrationBean result = new FilterRegistrationBean();
    result.setFilter(this.tenantFilter());
    result.setUrlPatterns(Lists.newArrayList("/*"));
    result.setName("Tenant Store Filter");
    result.setOrder(1);
    return result;
  }

  @Bean(destroyMethod = "destroy")
  public ThreadLocalTargetSource threadLocalTenantStore() {
    ThreadLocalTargetSource result = new ThreadLocalTargetSource();
    result.setTargetBeanName("tenantStore");
    return result;
  }

  @Primary
  @Bean(name = "proxiedThreadLocalTargetSource")
  public ProxyFactoryBean proxiedThreadLocalTargetSource(ThreadLocalTargetSource threadLocalTargetSource) {
    ProxyFactoryBean result = new ProxyFactoryBean();
    result.setTargetSource(threadLocalTargetSource);
    return result;
  }

  @Bean(name = "tenantStore")
  @Scope(scopeName = "prototype")
  public TenantStore tenantStore() {
    return new TenantStore();
  }
}

This is where Spring’s beans are instantiated. — tenantFilter() and tenantFilterRegistration() are straightforward. TenantFilter() instantiates a servlet filter, and tenantFilterRegistration() implements a Spring mechanism that allows dependencies to be injected in the  TenantFilter.java , a regular servlet filter.

Tip:Read this post  Implementing and Configuring Servlets, Filters, and Listeners in Spring Boot Applications for more information.

These other beans look interesting:

  •   threadLocalTenantStore   bean: The  ThreadLocalTargetSource  is useful when you need an object, a TenantStore.java instance in this case, to be created for each incoming request. The target (a TenantStore.java object) will be instantiated only once in each thread and will get removed from each thread’s  threadLocals  map when   ThreadLocalTargetSource ’s destroy() is called, for instance when the application is shut down.
  • The  tenantStore  bean is the target object stored in each thread. It is required to be prototype-scoped in the   AbstractPrototypeBasedTargetSource , parent class of the   ThreadLocalTargetSource .
  •   proxiedThreadLocalTargetSource  bean. According to the documentation and source code comments,  TargetSources  must run in a  BeanFactory  since they need to call the getBean() method to create a new prototype instance.

DemoResource.java :

package com.asimio.demo.web;
...
@RestController
@RequestMapping(value = "/demo")
public class DemoResource {

  @Autowired
  private TenantStore tenantStore;

  @RequestMapping(method = RequestMethod.GET)
  public String getDemo() {
    return String.format("Tenant: %s", this.tenantStore.getTenantId());
  }
}

This class implements a simple API when a TenantStore.java instance is injected.

We will see later that there is only one instance of the  TenantStore.java class for different requests, each holding different tenant data.

TenantFilter.java :

package com.asimio.demo.web;
...
public class TenantFilter implements Filter {
...
  @Autowired
  private TenantStore tenantStore;
...
  @Override
  public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
      throws  IOException, ServletException {

    // LOGGER.info("Thread had tenant data: {} from an old request", this.tenantStore.getTenantId());

    HttpServletRequest request = (HttpServletRequest) servletRequest;
    String tenantId = request.getHeader(TENANT_HEADER_NAME);
    try {
      this.tenantStore.setTenantId(tenantId);
      chain.doFilter(servletRequest, servletResponse);
    } finally {
      // Otherwise when a previously used container thread is used, it will have the old tenant id set and
      // if for some reason this filter is skipped, tenantStore will hold an unreliable value
      this.tenantStore.clear();
    }
  }
...

This servlet filter takes care of setting the tenant identifier to the TenantStore.java holder and clearing it up during the servlet filter chain’s way out.

We will see later how there is only one instance of the  TenantStore.java class for different requests, each holding different tenant data.

Packaging and Running the App

This application can be run from your preferred IDE as a regular Java application or from a command line:

cd <path to demo application>
mvn clean package
java -jar target/threadlocaltargetsource-demo.jar

Or, we can perform the following:

mvn spring-boot:run

Let’s place a breakpoint in the TenantFilter.java class:

tenant-filter-breakpoint.pngTenantFilter breakpoint

Then, we can send a couple of simultaneous requests to the /demo endpoint:

curl -v -H "X-TENANT-ID:tenant_1" "http://localhost:8080/demo"
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /demo HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
> X-TENANT-ID:tenant_1
>

And, we can also perform the following command:

curl -v -H "X-TENANT-ID:tenant_2" "http://localhost:8080/demo"
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /demo HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
> X-TENANT-ID:tenant_2
>

Both requests should have been suspended in the Tenant filter breakpoint . Let's take a look at partial stack traces and attribute values:

tenantfilter-breakpoint-request-1.png First request to /demo endpoint

Notice that tenantStore's id is 119 and that tenantStore's tenantId is set to tenant_1 .

tenantfilter-breakpoint-request-2.png Second request to /demo endpoint

As for the second request, tenantStore's id is also 119 but the tenantStore's tenantId is set to tenant_2 . Interesting, eh? All of this happens while both requests are still being processed. Also, notice http-nio-8080-exec-1 ’s stack trace corresponds to the processing request #1.

As the execution of the requests is resumed, the response to the first request looks like:

< HTTP/1.1 200
< Content-Type: text/plain;charset=UTF-8
< Content-Length: 16
< Date: Mon, 27 Nov 2017 05:41:23 GMT
<
* Connection #0 to host localhost left intact
Tenant: tenant_1

And, the response to the second request:

< HTTP/1.1 200
< Content-Type: text/plain;charset=UTF-8
< Content-Length: 16
< Date: Mon, 27 Nov 2017 05:41:23 GMT
<
* Connection #0 to host localhost left intact
Tenant: tenant_2

Let’s now see how a unit test for the DemoResource.java class would look:

DemoResourceTest.java :

package com.asimio.demo.web;
...
@RunWith(MockitoJUnitRunner.class)
public class DemoResourceTest {

  @Mock
  private TenantStore tenantStore;

  @InjectMocks
  private DemoResource controller;

  @Test
  public void shouldReturnBlahTenantId() {
    // Given
    BDDMockito.when(this.tenantStore.getTenantId()).thenReturn("blahTenantId");
    // When
    String result = this.controller.getDemo();
    // Then
    Assert.assertThat(result, Matchers.equalTo("Tenant: blahTenantId"));
  }
}

The TenantStore dependency is being mocked using Mockito and injected in the API implementation. A case could be made to prevent using  @InjectMocks but that goes out of the scope of this post.

Compare this:

DDMockito.when(this.tenantStore.getTenantId()).thenReturn("blahTenantId");
...

To how I used it earlier this year:

try {
  DvdRentalTenantContext.setTenantId("tenant_1");
  ...
} finally {
  DvdRentalTenantContext.clear();
}

This is error-prone since the ThreadLocal   would have to be cleared in every test that follows this pattern to prevent a possible misuse if another unit test uses the same  ThreadLocal ran in the same thread.

And, that’s all! Thanks for reading, and as always, your feedback is very much appreciated. If you found this post helpful and would like to receive updates when content like this gets published, sign up to the newsletter .

The source code for this blog post can be found here .

References

BloomReach CMS: the API-first CMS of the future. Open-source & enterprise-grade. - As a Java developer, you will feel at home using Maven builds and your favorite IDE (e.g. Eclipse or IntelliJ) and continuous integration server (e.g. Jenkins). Manage your Java objects using Spring Framework, write your templates in JSP or Freemarker. Try for free.

DOWNLOAD

Topics:

java , tutorial , spring , maven , spring boot , spring framework , tenantstore , thread , threadlocal


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK