by Emil Korladinov
Introduction
Projects with requirements to dynamically switch connections at runtime and exchange data with different datasources along with multi-tenat capabilities are very common.
For a project I worked on I compiled a solution using Spring Framework, Spring Data Jpa, Hibernate and Aspect Oriented Programming with Spring (SpringAOP).
Here I'd like to share my solution with all of you.
Implementation
Basic idea is to wrap unit of work into a transaction and to exexute it against preconfigured databases(datasources). Transactions are demarcated using standard Spring annotation-based transaction management. Since annotations are used to mark code which will use database routing a new annotation is introduced: @DatabaseRouting which is meta annotated with Spring @Transactional. Information for the currently used tenant id is kept in a ThreadLocal variable on a per-thread basis. On the database side a special DataSource is used - DatabaseRoutingDatasource which extends Spring's AbstractRoutingDataSource.
Working horse of the solution is aspect with an @Around pointcut. Second annotation, applied on method parameter is used to convey information for chosen datasource down to Spring transaction.
Annotations
Two annotations are used:
@DatabaseRouting is method level annotation marking method which uses database routing to dynamically select database connection and exchange data. Methods that have this anotation are wrapped in transaction. This annotation is matched by an around aspect which sets current connection discriminator (tenantId), proceeds with methd execution and resets connection to default.
This annotation is defined as follows:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Retention(RetentionPolicy.RUNTIME) | |
@Target(ElementType.METHOD) | |
@Transactional(propagation = Propagation.REQUIRES_NEW) | |
public @interface DatabaseRouting { | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Retention(RetentionPolicy.RUNTIME) | |
@Target(ElementType.PARAMETER) | |
public @interface TenantId { | |
} |
ThreadLocals
TenantIdHolder is standard Java ThreadLocal containing current value of tenantId.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class TenantIdHolder { | |
private static final ThreadLocal<String> holder = new ThreadLocal<>(); | |
public static String get() { | |
return holder.get(); | |
} | |
public static void set(String tenantId) { | |
if (null == tenantId) { | |
throw new NullPointerException(); | |
} | |
holder.set(tenantId); | |
} | |
public static void clear() { | |
holder.remove(); | |
} | |
} |
Datasources
RoutingDataSource Extends AbstractRoutingDataSource from spring, overriding method for key lookup (tenantId), linking it with TenantIdHolder.
Aspects
DatabaseRoutingAspect
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Component | |
@Aspect | |
@Order(200) | |
public class DatebaseRoutingAspect { | |
@Autowired | |
private Map dataSources; | |
@Pointcut("@annotation(DatabaseRouting))") | |
public void databaseRouting() { | |
} | |
@Pointcut("execution(* *(..))") | |
public void atExecution() { | |
} | |
@Around("@annotation(databaseRouting)") | |
public Object switchDatabase(ProceedingJoinPoint pjp, DatabaseRouting databaseRouting) throws Throwable { | |
String tenantId = retrieveTenantId(pjp); | |
try { | |
if (null != tenantId) { | |
if (!dataSources.containsKey(tenantId)) { | |
throw new IllegalArgumentException("Unknown database key: " + tenantId); | |
} | |
TenantIdHolder.set(tenantId); | |
} | |
return pjp.proceed(); | |
} finally { | |
TenantIdHolder.clear(); | |
} | |
} | |
private String retrieveTenantId(ProceedingJoinPoint pjp) { | |
MethodSignature signature = (MethodSignature) pjp.getSignature(); | |
Annotation[][] parameterAnnotations = signature.getMethod().getParameterAnnotations(); | |
Object[] args = pjp.getArgs(); | |
int idx = 0; | |
Object tenantId = null; | |
for (Annotation[] annotation : parameterAnnotations) { | |
tenantId = args[idx++]; | |
for (Annotation annot : annotation) { | |
if (TenantId.class.equals(annot.annotationType())) { | |
break; | |
} | |
} | |
} | |
return (String) tenantId; | |
} | |
} |
Hi.. i want help to make spring batch multi tenant. using JWT token.
ReplyDeleteHi, please give some more details and context
DeleteThanx