Mastering Data Isolation in Java: MyBatis Interceptor & Custom Annotations
This article walks through the challenges of environment‑specific data isolation in a Java project, explains why adding an env column to every table is impractical, and presents a clean solution using a MyBatis interceptor that rewrites SQL and custom annotations to control filtering, ensuring safe, maintainable code across pre‑release, gray, and production environments.
1. Historical Background
In the pre‑release, gray, and online environments a single database is shared, with each table containing an env column to distinguish data per environment.
1.1 Data Isolation
All three environments share the same tables, differentiated by the env field.
1.2 Before Isolation
Initially only one core table had the env column; later, about twenty tables needed the column to prevent pre‑release operations from affecting production data.
1.3 Isolation Refactor
Historical data cannot be easily distinguished, so the new env field is initialized to all, allowing legacy data to be accessed by all environments.
1.4 Isolation Scheme
The naive approach would be to add env to every DO, Mapper, and XML, but this is rejected.
1.5 Final Implementation
A custom MyBatis interceptor rewrites SQL: it fills the env value on inserts and adds an env condition on queries. Historical data is handled by using env in (${currentEnv},'all').
SELECT xxx FROM ${tableName} WHERE env in (${currentEnv},'all') AND ${otherCondition}The interceptor reads the environment value from application.properties and modifies the SQL accordingly.
2. Development Evolution
2.1 Business Requirements
PRC interface needs environment‑specific matching.
Some environments share data (pre‑release and gray).
Developers want to correct online data from pre‑release.
2.2 Initial Communication
The junior developer asked how to skip environment checks; suggestions included ignoring the field, always appending conditions, or using annotations.
2.3 Implementation
Use a ThreadLocal‑based context to store the environment filter and let the interceptor apply it.
@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) {
// handle insert
}
return invocation.proceed();
}
}2.4 Refactoring
After refactor, the code fills env with all environments, and queries add the appropriate filter.
SELECT * FROM ${tableName} WHERE env in ('pre','gray','online','all') AND ${otherCondition}2.5 Error Causes
Null env values were caused by multiple ThreadLocal manipulations where a downstream method cleared the context before the upstream method retrieved it.
2.6 Observations
Environment‑related code proliferated across the codebase, making maintenance hard.
2.7 Code Spread
String oriFilterEnv = UserHolder.getUser().getFilterEnv();
UserHolder.getUser().setFilterEnv(globalConfigDTO.getAllEnv());
// ...
UserHolder.getUser().setFilterEnv(oriFilterEnv);2.8 Soul Questions
Is this the only way? Could the Open/Closed principle be better respected? Are we mixing business logic with infrastructure concerns?
3. Refactoring
3.1 Challenges
The MyBatis interceptor cannot directly access service‑layer methods, requiring stack‑frame inspection.
3.2 Problem List
Avoid modifying existing methods.
Separate concerns between business and infrastructure.
Make changes in a single place.
Promote reuse instead of copy‑pasting.
3.3 Implementation Analysis
Use an independent ThreadLocal for environment filtering.
Define a custom annotation + AOP to declare skip rules.
Handle method‑to‑method calls and recursion carefully.
3.4 Use Cases
Annotate entry methods to skip environment checks for specific tables.
@InvokeChainSkipEnvRule(skipEnvList = {"pre"}, skipTableList = {"project"})
public void importSignedUserData(HttpServletRequest request, HttpServletResponse response) { ... }3.5 Implementation Details
Mark methods with the annotation.
AOP reads the annotation and stores rules in the application context.
The interceptor retrieves the rules and decides whether to apply the env filter.
3.6 Limitations
Granularity is coarse; all operations on a table are skipped.
Annotation can only be placed on entry points, not internal calls.
4. Summary & Reflection
4.1 Isolation Summary
The project demonstrates a practical approach to both data isolation and sharing by using a custom MyBatis interceptor and annotation‑driven AOP.
4.2 Coding Summary
Modify a single place instead of scattering changes.
Leverage custom annotations for cross‑cutting concerns.
Maintain a clear boundary between business and infrastructure code.
4.3 Scenario Summary
Scenario
Description
Distributed Lock
Use custom annotation to apply a lock on method execution.
Compliance Validation
Combine OGNL expressions with annotation to validate parameters.
API Data Permission
Different permissions per API based on annotation rules.
Routing Strategy
Route requests to different handlers via annotations.
4.4 Reflection
Early design decisions (e.g., separate databases) could avoid complex interceptors; always prioritize design before coding.
4.5 Final Thoughts
Custom annotations are flexible and widely applicable; explore their potential.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java High-Performance Architecture
Sharing Java development articles and resources, including SSM architecture and the Spring ecosystem (Spring Boot, Spring Cloud, MyBatis, Dubbo, Docker), Zookeeper, Redis, architecture design, microservices, message queues, Git, etc.
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.
