Backend Development 9 min read

Data Isolation and Environment Switching in MyBatis Using a Custom Interceptor and Annotations

This article describes how to achieve database environment isolation and flexible data sharing in a Java backend by adding an env field, customizing a MyBatis interceptor to rewrite SQL, and defining reusable annotations to control environment filtering across services.

Architect's Guide
Architect's Guide
Architect's Guide
Data Isolation and Environment Switching in MyBatis Using a Custom Interceptor and Annotations

The article begins with a historical background where pre‑release, gray, and production environments shared a single database, leading to the introduction of an env column in every table to distinguish data per environment.

Before isolation, only one core table had the env field; later, dozens of tables required the field, demanding a migration strategy that preserved existing data while ensuring safety.

To avoid manually adding the field to every DO, Mapper, and XML, a custom MyBatis interceptor was created. The interceptor rewrites SQL at runtime: for INSERT statements it fills the env value, and for SELECT statements it adds a condition env in (${currentEnv},'all') to support both new and historical data.

Example SQL generated by the interceptor:

SELECT xxx FROM ${tableName} WHERE env in (${currentEnv},'all') AND ${otherCondition}

The core interceptor code looks like this:

@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 e) {
                log.error("parser insert sql exception, boundSql is:" + JSON.toJSONString(boundSql), e);
                throw e;
            }
        }
        return invocation.proceed();
    }
}

For finer‑grained control, a custom annotation @InvokeChainSkipEnvRule was defined. Developers can place the annotation on controller or service methods to specify which environments or tables should skip the env filter.

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface InvokeChainSkipEnvRule {
    boolean isKip() default true;
    String[] skipEnvList() default {};
    String[] skipTableList() default {};
}

Usage example:

@InvokeChainSkipEnvRule(skipEnvList = {"pre"}, skipTableList = {"project"})
@GetMapping("/importSignedUserData")
public void importSignedUserData(HttpServletRequest request, HttpServletResponse response) { ... }

The article also discusses common pitfalls such as ThreadLocal misuse, the need to keep business logic separate from infrastructure code, and the importance of minimal invasive changes to maintain code stability.

In the concluding sections, the author reflects on the benefits of using custom annotations and AOP for cross‑cutting concerns like data isolation, distributed locks, and permission checks, urging developers to refactor duplicated code and adopt a design‑first approach.

backendJavaMyBatisCustom AnnotationData IsolationInterceptor
Architect's Guide
Written by

Architect's Guide

Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.

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.