Backend Development 13 min read

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.

Java Architect Essentials
Java Architect Essentials
Java Architect Essentials
Implementing Data Isolation in Java Applications Using MyBatis Interceptor and Custom Annotations

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.

backendJavaAOPMyBatisCustom AnnotationData IsolationSQL Rewrite
Java Architect Essentials
Written by

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.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.