Six Years In, This Java Code Made Me Cringe
The article recounts a six‑year veteran’s encounter with a tangled Java data‑isolation implementation, explains how a custom MyBatis interceptor and annotation were used to inject and filter an env field across dozens of tables, and reflects on the code smell and refactoring lessons learned.
1. Historical Background
1.1 Data Isolation
In the pre‑release, gray, and online environments a single database is shared. Each table contains an env column indicating the environment, as shown in the diagram.
1.2 Before Isolation
Initially only one core table had the env column; the other twenty‑plus tables lacked it. An operation in the pre‑release environment once polluted production data, prompting the addition of the env column to all tables.
1.3 Isolation Refactor
Historical data are hard to separate, so the new env field is initialized to all, allowing both pre‑release and online environments to access old records.
1.4 Bad Approach
The worst practice is to add the env column to every DO, Mapper, and XML file individually.
1.5 Solution
A custom MyBatis interceptor handles the env logic uniformly, so business code (DO, Mapper, XML) does not need to be modified.
Business code remains untouched.
Avoids the massive effort and high error rate of adding the column manually.
Facilitates future extensions.
1.6 Implementation
SQL is rewritten to include the environment condition:
SELECT XXX FROM tableName WHERE env = ${environment} AND ${condition}During insertion the env value is set to all for compatibility, and queries use an IN clause:
SELECT xxx FROM ${tableName} WHERE env IN (${currentEnv},'all') AND ${otherCondition}Interceptor code (partial):
@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 {
insertMethodProcess(invocation, boundSql);
} catch (Exception exception) {
log.error("parser insert sql exception, boundSql is:" + JSON.toJSONString(boundSql), exception);
throw exception;
}
}
return invocation.proceed();
}
}1.7 Error Cause
Investigation revealed that multiple places manipulate a ThreadLocal holding the environment. When method B clears the context after returning to method A, A receives a null env value.
1.8 Proliferation
Similar env‑handling code appears throughout the business layer:
String oriFilterEnv = UserHolder.getUser().getFilterEnv();
UserHolder.getUser().setFilterEnv(globalConfigDTO.getAllEnv());
UserHolder.getUser().setFilterEnv(oriFilterEnv);1.9 Open Questions
Does this violate the Open‑Closed Principle?
What about missed fields?
Are other teams using similar shortcuts?
Is business logic properly separated from functional code?
Is storing the env in the user context appropriate?
2. Evolution
2.1 Business Requirements
PRC interface needs different env handling for upstream partners.
Some environments share data (pre‑release, gray).
Developers want to correct online data from pre‑release.
2.2 Initial Discussion
A junior developer asked for guidance. Suggestions included skipping env checks, concatenating all conditions, or using annotations to mark specific methods.
2.3 Implementation Details
Hard‑coded env list leads to queries like:
SELECT * FROM ${tableName} WHERE env IN ('pre','gray','online','all') AND ${otherCondition}2.4 Annotation Usage
Define a custom annotation to declare skip rules:
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface InvokeChainSkipEnvRule {
boolean isKip() default true;
String[] skipEnvList() default {};
String[] skipTableList() default {};
}Apply it on controller methods:
@InvokeChainSkipEnvRule(skipEnvList = {"pre"}, skipTableList = {"project"})
@GetMapping("/importSignedUserData")
public void importSignedUserData(HttpServletRequest request, HttpServletResponse response) {
// ...
}2.5 Limitations
The rule applies to the whole call chain, giving coarse granularity.
Annotations can only be placed on entry points; internal calls should avoid them.
3. Refactoring Thoughts
3.1 Difficulties
MyBatis interceptors cannot directly obtain the service‑layer method; they must inspect the stack trace to locate the caller.
3.2 Guidelines
Do not modify existing methods unless necessary.
Keep business and functional code separate.
Aim for minimal changes – a single place should implement the logic.
Ensure the refactored code is reusable, not a copy‑paste solution.
3.3 Implementation Analysis
Use an independent ThreadLocal instead of mixing with user context.
Combine annotation with AOP to parse parameters and enforce the rule.
Consider optimization for recursive or repeated calls.
3.4 Use Case
Skip env check for the project table in pre‑release:
@InvokeChainSkipEnvRule(skipEnvList = {"pre"}, skipTableList = {"project"})
public void someMethod() { /* ... */ }3.5 Summary of Scenarios
Distributed lock via custom annotation.
Compliance parameter validation using OGNL expressions.
Interface data permission based on user roles.
Routing strategies by annotating handlers.
4. Final Thoughts
4.1 Isolation Summary
The case demonstrates a practical approach to both data isolation and sharing by combining a custom MyBatis interceptor with annotation‑driven AOP.
4.2 Coding Summary
Modify in one place instead of scattering similar code.
Leverage custom annotations to encapsulate common logic.
Compromise when needed, but keep clear boundaries.
4.3 Reflection
Early design decisions, such as using separate databases or rejecting the requirement outright, could avoid the technical debt observed.
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.
Architect's Guide
Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.
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.
