Implementing Data Isolation in Java Applications Using MyBatis Interceptor and Custom Annotations
This article describes a Java data isolation solution that adds an 'env' field to tables, uses a MyBatis interceptor to rewrite SQL for environment‑aware queries, and introduces custom annotations with AOP to selectively skip environment checks, detailing implementation, challenges, and best practices.
Seeing Such Code, Mixed Feelings...
In an afternoon the author encountered a piece of Java code that triggered strong emotions and prompted a deep reflection on data isolation practices.
Historical Background
Data Isolation
Pre‑release, gray, and online environments shared a single database. Each table contained an env column indicating the environment (e.g., pre , gray , online ). The diagram below shows the original schema.
Before Isolation
Initially only one core table had the env column; the remaining twenty‑plus tables did not. A production incident caused pre‑release operations to affect online data, so the env column had to be added to all tables.
Isolation Transformation
Historical data could not be distinguished easily, so the new env column was initialized with the value all , meaning the data is visible in all environments. This preserves compatibility with existing records.
Isolation Solution
The naive approach would be to add env to every DO, Mapper, and XML file, but the author rejected this. Instead, a custom MyBatis interceptor was created to handle the logic centrally.
The interceptor provides three main benefits:
Business code does not need to be modified (DO, Mapper, XML remain unchanged).
Avoids massive manual additions and reduces error probability.
Facilitates future extensions.
Final Implementation
Within the MyBatis interceptor, SQL is rewritten: on INSERT the env field is populated, and on SELECT the env condition is added. Historical data uses env = 'all' for compatibility.
SELECT xxx FROM ${tableName} WHERE env in (${当前环境},'all') AND ${其他条件}The env value is read from application.properties and is globally unique per environment.
The interceptor leverages the open‑source JSqlParser to parse and modify SQL statements.
@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})})
@Component
public class EnvIsolationInterceptor implements Interceptor {
...
@Override
public Object intercept(Invocation invocation) throws Throwable {
if (SqlCommandType.INSERT == sqlCommandType) {
try {
// rewrite SQL and fill env parameter
insertMethodProcess(invocation, boundSql);
} catch (Exception e) {
log.error("parser insert sql exception, boundSql is:" + JSON.toJSONString(boundSql), e);
throw e;
}
}
return invocation.proceed();
}
}Development Evolution
Business Requirements
As the system grew, new requirements emerged:
PRC interfaces needed different environment matching.
Some environments (e.g., pre‑release and gray) should share data.
Developers wanted to correct online data from pre‑release.
SELECT * FROM ${tableName} WHERE bizId = ${bizId} and env in (?, 'all')Initial Discussion
A junior developer asked how to skip the environment check. Possible ideas included omitting the env condition, always appending it, or using an annotation to mark specific methods.
Implementation Details
The chosen approach uses a custom annotation and AOP to control the interceptor behavior.
SELECT * FROM ${tableName} WHERE env in ('pre','gray','online','all') AND ${其他条件}Error Analysis
The root cause of a null environment value was traced to multiple ThreadLocal manipulations that cleared the context after nested method calls.
Code Scattered Everywhere
Typical code pattern that appears throughout the business layer:
// 1. Save original env
String oriFilterEnv = UserHolder.getUser().getFilterEnv();
// 2. Set global env for the request
UserHolder.getUser().setFilterEnv(globalConfigDTO.getAllEnv());
// ... business logic ...
// 3. Restore original env
UserHolder.getUser().setFilterEnv(oriFilterEnv);Refactoring
Challenges
The interceptor cannot directly obtain the service‑layer method being invoked; it must rely on stack frames.
Problem List
Avoid modifying existing methods.
Separate concerns between business logic and infrastructure.
Make the change in a single place.
Provide reusable capability rather than ad‑hoc code.
Implementation Analysis
Use an independent ThreadLocal instead of mixing with user context.
Define a custom annotation + AOP to convey skip rules.
Consider optimization for nested or recursive calls.
Usage Example
After refactoring, a controller method can be annotated to skip environment checks for the project table in the pre environment:
@InvokeChainSkipEnvRule(skipEnvList = {"pre"}, skipTableList = {"project"})
@SneakyThrows
@GetMapping("/importSignedUserData")
public void importSignedUserData(HttpServletRequest request, HttpServletResponse response) {
// ...
}Annotation Definition
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface InvokeChainSkipEnvRule {
/** Whether to skip the environment check. Default true. */
boolean isKip() default true;
/** List of environments to skip. */
String[] skipEnvList() default {};
/** List of tables to skip. */
String[] skipTableList() default {};
}Limitations
The rule applies to the whole call chain of the annotated method, which may be too coarse‑grained.
The annotation can only be placed on entry points; internal helper methods should avoid using it.
Summary and Reflection
Isolation Summary
This case demonstrates a practical combination of data isolation (via env column) and data sharing (via all value) using a custom MyBatis interceptor and annotation‑driven AOP.
Coding Summary
Repeated logic should be refactored into a single place; custom annotations provide a clean way to express cross‑cutting concerns.
Scenario Summary
Custom annotation + AOP is a flexible pattern applicable to many similar requirements.
Reflection
Early technical design (or separate databases) could avoid the need for such complex workarounds.
Consider rejecting unreasonable requirements when they threaten data safety.
Design first, then code; avoid ad‑hoc solutions.
Final Thoughts
The author reflects on the emotional toll of maintaining such intricate solutions and emphasizes the importance of thoughtful architecture.
To conclude, the article invites readers to share, join the community, and continue learning.
Java Architect Essentials
Committed to sharing quality articles and tutorials to help Java programmers progress from junior to mid-level to senior architect. We curate high-quality learning resources, interview questions, videos, and projects from across the internet to help you systematically improve your Java architecture skills. Follow and reply '1024' to get Java programming resources. Learn together, grow together.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.