Mybatis plus official artifact release!

Today, we will introduce an artifact officially released by MyBatis - Plus: mybatis mate is an mp enterprise level module that supports database and table separation, data audit, data sensitive word filtering (AC algorithm), field encryption, dictionary write back (data binding), data permissions, automatic SQL maintenance of table structure, etc., aiming at more agile and elegant data processing.

major function

  • Dictionary binding
  • Field encryption
  • Data desensitization
  • Table structure dynamic maintenance
  • Data audit records
  • Data range (Data permission)
  • The database is divided into databases and tables, dynamic data sources, read-write separation, and automatic switching of data database health check.

2. Use

2.1 dependency import

Spring Boot introduces automatic dependency annotation package

<dependency>
  <groupId>com.baomidou</groupId>
  <artifactId>mybatis-mate-starter</artifactId>
  <version>1.0.8</version>
</dependency>

Notes (for entity subcontracting)

<dependency>
  <groupId>com.baomidou</groupId>
  <artifactId>mybatis-mate-annotation</artifactId>
  <version>1.0.8</version>
</dependency>

2.2 field data binding (Dictionary write back)

For example, user_sex type sex dictionary result mapped to sexText property

@FieldDict(type = "user_sex", target = "sexText")
private Integer sex;

private String sexText;

Implement the IDataDict interface to provide the dictionary data source and inject it into the Spring container.

@Component
public class DataDict implements IDataDict {

    /**
     * Get from database or cache
     */
    private Map<String, String> SEX_MAP = new ConcurrentHashMap<String, String>() {{
        put("0", "female");
        put("1", "male");
    }};

    @Override
    public String getNameByCode(FieldDict fieldDict, String code) {
        System.err.println("Field type:" + fieldDict.type() + ",code:" + code);
        return SEX_MAP.get(code);
    }
}

2.3 field encryption

The attribute @FieldEncrypt annotation can be encrypted and stored, and the query results will be automatically decrypted. It supports the global configuration of encryption key algorithm and annotation key algorithm, and can implement the IEncryptor injection custom algorithm.

@FieldEncrypt(algorithm = Algorithm.PBEWithMD5AndDES)
private String password;

2.4 field desensitization

Attribute @FieldSensitive annotation can automatically desensitize the source data according to the preset policy. By default, 9 common desensitization policies are built into the sentitivetype.

For example, desensitization strategies such as Chinese name, bank card account number and mobile phone number. You can also customize the policy as follows:

@FieldSensitive(type = "testStrategy")
private String username;

@FieldSensitive(type = SensitiveType.mobile)
private String mobile;

Add the custom desensitization policy testStrategy to the default policy and inject it into the Spring container.

@Configuration
public class SensitiveStrategyConfig {

    /**
     * Injection desensitization strategy
     */
    @Bean
    public ISensitiveStrategy sensitiveStrategy() {
        // Custom testStrategy type desensitization
        return new SensitiveStrategy().addStrategy("testStrategy", t -> t + "***test***");
    }
}

For example, article sensitive word filtering

/**
 * Demonstration Article sensitive word filtering
 */
@RestController
public class ArticleController {
    @Autowired
    private SensitiveWordsMapper sensitiveWordsMapper;

    // Test access to the following address to observe the request address, interface return data and console (common parameters)
    // No sensitive words http://localhost:8080/info?content=tom&see=1&age=18
    // English sensitive words http://localhost:8080/info?content=my%20content%20is%20tomcat&see=1&age=18
    // Chinese sensitive words http://localhost:8080/info?content=%E7%8E%8B%E5%AE%89%E7%9F%B3%E5%94%90%E5%AE%8B%E5%85%AB%E5%A4%A7%E5%AE%B6&see=1
    // Multiple sensitive words http://localhost:8080/info?content=%E7%8E%8B%E5%AE%89%E7%9F%B3%E6%9C%89%E4%B8%80%E5%8F%AA%E7%8C%ABtomcat%E6%B1%A4%E5%A7%86%E5%87%AF%E7%89%B9&see=1&size=6
    // Insert a word to make it insensitive http://localhost:8080/info?content=%E7%8E%8B%E7%8C%AB%E5%AE%89%E7%9F%B3%E6%9C%89%E4%B8%80%E5%8F%AA%E7%8C%ABtomcat%E6%B1%A4%E5%A7%86%E5%87%AF%E7%89%B9&see=1&size=6
    @GetMapping("/info")
    public String info(Article article) throws Exception {
        return ParamsConfig.toJson(article);
    }


    // Add a sensitive word and see if it works http://localhost:8080/add
    // Observe that the word "cat" is filtered http://localhost:8080/info?content=%E7%8E%8B%E5%AE%89%E7%9F%B3%E6%9C%89%E4%B8%80%E5%8F%AA%E7%8C%ABtomcat%E6%B1%A4%E5%A7%86%E5%87%AF%E7%89%B9&see=1&size=6
    // Nested sensitive word processing http://localhost:8080/info?content=%E7%8E%8B%E7%8C%AB%E5%AE%89%E7%9F%B3%E6%9C%89%E4%B8%80%E5%8F%AA%E7%8C%ABtomcat%E6%B1%A4%E5%A7%86%E5%87%AF%E7%89%B9&see=1&size=6
    // Multi nested sensitive words http://localhost:8080/info?content=%E7%8E%8B%E7%8E%8B%E7%8C%AB%E5%AE%89%E7%9F%B3%E5%AE%89%E7%9F%B3%E6%9C%89%E4%B8%80%E5%8F%AA%E7%8C%ABtomcat%E6%B1%A4%E5%A7%86%E5%87%AF%E7%89%B9&see=1&size=6
    @GetMapping("/add")
    public String add() throws Exception {
        Long id = 3L;
        if (null == sensitiveWordsMapper.selectById(id)) {
            System.err.println("Insert a sensitive word:" + sensitiveWordsMapper.insert(new SensitiveWords(id, "cat")));
            // Insert a sensitive word and refresh the sensitive word of the algorithm engine
            SensitiveWordsProcessor.reloadSensitiveWords();
        }
        return "ok";
    }

    // Test access the following address observation console (request json parameters)
    // idea executes the resources directory testjson Http file test
    @PostMapping("/json")
    public String json(@RequestBody Article article) throws Exception {
        return ParamsConfig.toJson(article);
    }
}

2.5 automatic maintenance of DDL data structure

Solve the problem of upgrading table structure initialization and updating SQL maintenance after version release. Currently, MySql and PostgreSQL are supported.

@Component
public class PostgresDdl implements IDdl {

    /**
     * Execute SQL script mode
     */
    @Override
    public List<String> getSqlFiles() {
        return Arrays.asList(
                // Built in package method
                "db/tag-schema.sql",
                // File absolute path mode
                "D:\\db\\tag-data.sql"
        );
    }
}

Not only can it be executed in a fixed way, but also it can be executed dynamically!!

ddlScript.run(new StringReader("DELETE FROM user;\n" +
                "INSERT INTO user (id, username, password, sex, email) VALUES\n" +
                "(20, 'Duo', '123456', 0, 'Duo@baomidou.com');"));

It also supports multi data source execution!!!

@Component
public class MysqlDdl implements IDdl {

    @Override
    public void sharding(Consumer<IDdl> consumer) {
        // Multi data source specification, master library initialization, slave library automatic synchronization
        String group = "mysql";
        ShardingGroupProperty sgp = ShardingKey.getDbGroupProperty(group);
        if (null != sgp) {
            // Main library
            sgp.getMasterKeys().forEach(key -> {
                ShardingKey.change(group + key);
                consumer.accept(this);
            });
            // From library
            sgp.getSlaveKeys().forEach(key -> {
                ShardingKey.change(group + key);
                consumer.accept(this);
            });
        }
    }

    /**
     * Execute SQL script mode
     */
    @Override
    public List<String> getSqlFiles() {
        return Arrays.asList("db/user-mysql.sql");
    }
}

2.6 dynamic multi data source master-slave free switching

@Sharding annotations enable the data source to be used and switched freely. You can add annotations in the mapper layer and hit whatever you want!!

@Mapper
@Sharding("mysql")
public interface UserMapper extends BaseMapper<User> {

    @Sharding("postgres")
    Long selectByUsername(String username);
}

You can also customize the strategy for unified deployment

@Component
public class MyShardingStrategy extends RandomShardingStrategy {

    /**
     * Decide to switch data source key {@link ShardingDatasource}
     *
     * @param group Dynamic database group
     * @param invocation {@link Invocation}
     * @param sqlCommandType {@link SqlCommandType}
     */
    @Override
    public void determineDatasourceKey(String group, Invocation invocation, SqlCommandType sqlCommandType) {
        // The data source group can be selected by user. The keys are the master-slave nodes in the data source group, which can be selected randomly or controlled by yourself
        this.changeDatabaseKey(group, sqlCommandType, keys -> chooseKey(keys, invocation));
    }
}

You can start the master-slave strategy, and of course, you can start the health check! Specific configuration:

mybatis-mate:
  sharding:
    health: true # Health testing
    primary: mysql # Default select data source
    datasource:
      mysql: # Database group
        - key: node1
          ...
        - key: node2
          cluster: slave # The master database is responsible for sql query operations during the separation of read and write from the database. The master database may not write by default
          ...
      postgres:
        - key: node1 # Data node
          ...

2.7 distributed transaction log printing

Some configurations are as follows:

/**
 * <p>
 * Performance analysis interceptor, used to output each SQL statement and its execution time
 * </p>
 */
@Slf4j
@Component
@Intercepts({@Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}),
        @Signature(type = StatementHandler.class, method = "update", args = {Statement.class}),
        @Signature(type = StatementHandler.class, method = "batch", args = {Statement.class})})
public class PerformanceInterceptor implements Interceptor {
    /**
     * SQL The maximum execution time exceeds the automatic shutdown, which is helpful to find problems.
     */
    private long maxTime = 0;
    /**
     * SQL Format or not
     */
    private boolean format = false;
    /**
     * Whether to write log file <br>
     * true Write the log file without blocking the program execution< br>
     * Abnormal prompt for exceeding the set maximum execution time!
     */
    private boolean writeInLog = false;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Statement statement;
        Object firstArg = invocation.getArgs()[0];
        if (Proxy.isProxyClass(firstArg.getClass())) {
            statement = (Statement) SystemMetaObject.forObject(firstArg).getValue("h.statement");
        } else {
            statement = (Statement) firstArg;
        }
        MetaObject stmtMetaObj = SystemMetaObject.forObject(statement);
        try {
            statement = (Statement) stmtMetaObj.getValue("stmt.statement");
        } catch (Exception e) {
            // do nothing
        }
        if (stmtMetaObj.hasGetter("delegate")) {//Hikari
            try {
                statement = (Statement) stmtMetaObj.getValue("delegate");
            } catch (Exception e) {

            }
        }

        String originalSql = null;
        if (originalSql == null) {
            originalSql = statement.toString();
        }
        originalSql = originalSql.replaceAll("[\\s]+", " ");
        int index = indexOfSqlStart(originalSql);
        if (index > 0) {
            originalSql = originalSql.substring(index);
        }

        // Calculate SQL execution time
        long start = SystemClock.now();
        Object result = invocation.proceed();
        long timing = SystemClock.now() - start;

        // Format SQL print execution results
        Object target = PluginUtils.realTarget(invocation.getTarget());
        MetaObject metaObject = SystemMetaObject.forObject(target);
        MappedStatement ms = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
        StringBuilder formatSql = new StringBuilder();
        formatSql.append(" Time: ").append(timing);
        formatSql.append(" ms - ID: ").append(ms.getId());
        formatSql.append("\n Execute SQL: ").append(sqlFormat(originalSql, format)).append("\n");
        if (this.isWriteInLog()) {
            if (this.getMaxTime() >= 1 && timing > this.getMaxTime()) {
                log.error(formatSql.toString());
            } else {
                log.debug(formatSql.toString());
            }
        } else {
            System.err.println(formatSql);
            if (this.getMaxTime() >= 1 && timing > this.getMaxTime()) {
                throw new RuntimeException(" The SQL execution time is too large, please optimize ! ");
            }
        }
        return result;
    }

    @Override
    public Object plugin(Object target) {
        if (target instanceof StatementHandler) {
            return Plugin.wrap(target, this);
        }
        return target;
    }

    @Override
    public void setProperties(Properties prop) {
        String maxTime = prop.getProperty("maxTime");
        String format = prop.getProperty("format");
        if (StringUtils.isNotEmpty(maxTime)) {
            this.maxTime = Long.parseLong(maxTime);
        }
        if (StringUtils.isNotEmpty(format)) {
            this.format = Boolean.valueOf(format);
        }
    }

    public long getMaxTime() {
        return maxTime;
    }

    public PerformanceInterceptor setMaxTime(long maxTime) {
        this.maxTime = maxTime;
        return this;
    }

    public boolean isFormat() {
        return format;
    }

    public PerformanceInterceptor setFormat(boolean format) {
        this.format = format;
        return this;
    }

    public boolean isWriteInLog() {
        return writeInLog;
    }

    public PerformanceInterceptor setWriteInLog(boolean writeInLog) {
        this.writeInLog = writeInLog;
        return this;
    }

    public Method getMethodRegular(Class<?> clazz, String methodName) {
        if (Object.class.equals(clazz)) {
            return null;
        }
        for (Method method : clazz.getDeclaredMethods()) {
            if (method.getName().equals(methodName)) {
                return method;
            }
        }
        return getMethodRegular(clazz.getSuperclass(), methodName);
    }

    /**
     * Get the beginning of sql statement
     *
     * @param sql
     * @return
     */
    private int indexOfSqlStart(String sql) {
        String upperCaseSql = sql.toUpperCase();
        Set<Integer> set = new HashSet<>();
        set.add(upperCaseSql.indexOf("SELECT "));
        set.add(upperCaseSql.indexOf("UPDATE "));
        set.add(upperCaseSql.indexOf("INSERT "));
        set.add(upperCaseSql.indexOf("DELETE "));
        set.remove(-1);
        if (CollectionUtils.isEmpty(set)) {
            return -1;
        }
        List<Integer> list = new ArrayList<>(set);
        Collections.sort(list, Integer::compareTo);
        return list.get(0);
    }

    private final static SqlFormatter sqlFormatter = new SqlFormatter();

    /**
     * Format sql
     *
     * @param boundSql
     * @param format
     * @return
     */
    public static String sqlFormat(String boundSql, boolean format) {
        if (format) {
            try {
                return sqlFormatter.format(boundSql);
            } catch (Exception ignored) {
            }
        }
        return boundSql;
    }
}

use:

@RestController
@AllArgsConstructor
public class TestController {
    private BuyService buyService;

    // Database test table t_order cannot insert data when the transaction is consistent. If it can, it means that the multi data source transaction is invalid
    // Test access http://localhost:8080/test
    // Manufacturing transaction rollback http://localhost:8080/test?error=true You can also make errors by modifying the table structure
    // Note ShardingConfig injects dataSourceProvider to test invalid transaction
    @GetMapping("/test")
    public String test(Boolean error) {
        return buyService.buy(null != error && error);
    }
}

2.8 Data permission

Add annotations to the mapper layer:

// test type data permission range, mixed paging mode
@DataScope(type = "test", value = {
        // Association table user alias u specifies Department field permissions
        @DataColumn(alias = "u", name = "department_id"),
        // The user alias u in the association table specifies the mobile phone number field (handle it by yourself)
        @DataColumn(alias = "u", name = "mobile")
})
@Select("select u.* from user u")
List<User> selectTestList(IPage<User> page, Long id, @Param("name") String username);

Simulate business processing logic:

@Bean
public IDataScopeProvider dataScopeProvider() {
    return new AbstractDataScopeProvider() {
        @Override
        protected void setWhere(PlainSelect plainSelect, Object[] args, DataScopeProperty dataScopeProperty) {
            // args contains the request parameters of the mapper method, which can be obtained by itself
            /*
                // Test data permissions and finally execute SQL statements
                SELECT u.* FROM user u WHERE (u.department_id IN ('1', '2', '3', '5'))
                AND u.mobile LIKE '%1533%'
             */
            if ("test".equals(dataScopeProperty.getType())) {
                // Business test type
                List<DataColumnProperty> dataColumns = dataScopeProperty.getColumns();
                for (DataColumnProperty dataColumn : dataColumns) {
                    if ("department_id".equals(dataColumn.getName())) {
                        // Append Department field IN condition, or SQL statement
                        Set<String> deptIds = new HashSet<>();
                        deptIds.add("1");
                        deptIds.add("2");
                        deptIds.add("3");
                        deptIds.add("5");
                        ItemsList itemsList = new ExpressionList(deptIds.stream().map(StringValue::new).collect(Collectors.toList()));
                        InExpression inExpression = new InExpression(new Column(dataColumn.getAliasDotName()), itemsList);
                        if (null == plainSelect.getWhere()) {
                            // No where condition exists
                            plainSelect.setWhere(new Parenthesis(inExpression));
                        } else {
                            // where conditions exist and processing
                            plainSelect.setWhere(new AndExpression(plainSelect.getWhere(), inExpression));
                        }
                    } else if ("mobile".equals(dataColumn.getName())) {
                        // Support a custom condition
                        LikeExpression likeExpression = new LikeExpression();
                        likeExpression.setLeftExpression(new Column(dataColumn.getAliasDotName()));
                        likeExpression.setRightExpression(new StringValue("%1533%"));
                        plainSelect.setWhere(new AndExpression(plainSelect.getWhere(), likeExpression));
                    }
                }
            }
        }
    };
}

Final execute SQL output:

SELECT u.* FROM user u 
  WHERE (u.department_id IN ('1', '2', '3', '5')) 
  AND u.mobile LIKE '%1533%' LIMIT 1, 10

Currently, there is only a paid version. For more mybatis mate usage examples, see:

https://gitee.com/baomidou/mybatis-mate-examples

< END >

Posted by tleisher on Thu, 02 Jun 2022 11:16:27 +0530