在工作中,为了保证数据安全,需要对数据库字段进行加解密,之前工作中就遇到了这种情况,因为线上数据库有很多的人都有权限,运维,账务,运营(通过后台系统查看),出口太多了,但有用户向我们平台举报,说有骚扰电话 ,能精准的知道用户姓名,手机号, 身份证号 ,以及一些用户在我们平台的账户数据,没有办法,数据被泄漏了,肯定不是阿里云数据库不安全导致的, 而是内部人员将数据给泄漏出去,因为出口太多,人员也太多,不知道怎么查询,公司决定将数据库中所有的用户的姓名,手机号, 身份证三要素加密,从数据库中无法看到三要素的名文,从而缩小数据导出人群的范围,因为这样做可能只有开发人员才能导出了。 其他运营,运维都只能看到加密的数据,如果没有密钥的话,是肯定解密不了数据,而后台做了一个功能,能将密文的excel导入,并转化为明文,但每一次解密的条数有限制,而且每次谁使用了导出明文功能,将消息发送钉钉群,这样一般人基本上就拿不到明文数据了,从而保证了数据的安全性。
理想确实是好的,但是这却累死开发人员了,项目中几十张表中的字段都需要加密解密, 相当于每一张表的插入,查询出的数据,查询条件都需要加密,这工作量非常的大,同时为后期的维护带来无穷尽的麻烦 , 每一次插入,查询条件,查询得出的结果都要想到这个字段是否是加密字段,如果是,则需要做相关的转化,几十张表都要记下来,太麻烦了, 同时新加的表也要考虑这些事情,而原本整洁的代码可能会因为加上这些加密解密变得臃肿不堪,有没有一种好的方案来解决这个问题呢?如开发人员只需要配置哪些表的哪些字段需要加密解密,而在代码中不需要做任何修改,保证保存到数据库中的是密文,取出的是名文,而查询也没有影响,按原来的操作即可, 插件会根据需要将相应的查询条件进行加密,如果这能实现那该多好啊,对开发人员没有任何工作量,而扩展性和维护性那就勿庸置疑了,如果想实现这样一个功能,该怎样做呢?
既然有了理想,接下来就是怎么去实现这样一款插件的功能了,再看插件如何开发之前,我们先来看一下插件的效果 。
CREATE TABLE lt_user_phone
(
id
bigint(20) NOT NULL AUTO_INCREMENT COMMENT ‘主键,自增id’,
is_delete
tinyint(4) NOT NULL DEFAULT ‘0’ COMMENT ‘是否删除状态,1:删除,0:有效’,
gmt_create
datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT ‘创建时间’,
gmt_modified
datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT ‘最后修改时间’,
user_name_en
varchar(64) NOT NULL DEFAULT ‘’,
real_name_en
varchar(64) NOT NULL DEFAULT ‘’,
unique_code
varchar(32) NOT NULL DEFAULT ‘’,
cnl_code
varchar(64) DEFAULT ‘’,
type
tinyint(4) NOT NULL DEFAULT ‘1’,
PRIMARY KEY (id
),
KEY Index_uniqueCode
(unique_code
) USING BTREE,
KEY index_user_name_en
(user_name_en
) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COMMENT=‘用户表’;
CREATE TABLE lt_stage_borrow
(
id
bigint(11) unsigned NOT NULL AUTO_INCREMENT COMMENT ‘自增主键’,
is_delete
bigint(4) NOT NULL DEFAULT ‘0’ COMMENT ‘是否删除’,
gmt_create
datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
gmt_modified
datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
unique_code
varchar(20) DEFAULT NULL,
PRIMARY KEY (id
),
KEY idx_borrowcach_uid
(unique_code
)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COMMENT=‘分期借款表’;
【注意】lt_user_phone 和 lt_stage_borrow 这两个表名为了和公司的用户表,记录表有所区别,所以这两张表名是我随意取的,字段为了和之前的有所区别,因此字段名也是随便弄的,方便和公司的业务表区分开,所以大家看起来可能觉得费解,如lt_stage_borrow 表存一个user_id 不就可以了不?还用unique_code来关联有什么意义。就是为了和公司相关的信息抹除,所以大家只关注技术即可,不需要关注字段含义。
@Slf4j @Configuration public class MybatisPluginConfig { //初始化变量 @PostConstruct public void postConstruct() { log.info("开始初始化表数据"); EncryptTableConfig.tableConfig.put("lt_user_phone", new PPTuple(Arrays.asList(new String[]{"real_name_en", "user_name_en", "id_number_en"}), Arrays.asList(new String[]{}))); } }
配置文件的目的就是配置lt_user_phone表的real_name_en,user_name_en,id_number_en字段需要加密解密字段
@Bean(name = "sqlSessionFactory") public SqlSessionFactory initSqlSessionFactory( PaginationInterceptor paginationInterceptor) throws Exception { MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean(); sessionFactory.setDataSource(initDataSource()); sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver() .getResources(env.getProperty("mybatis-plus.mapper-locations"))); sessionFactory.setConfigLocation(new ClassPathResource("mybatis-config.xml")); Interceptor[] interceptor = {paginationInterceptor,new DataScopeInterceptor(),new DataPrepareEncryptScopeInterceptor(), new DataEncryptScopeInterceptor(),new RestoreDataScopeInterceptor(),new QueryDecryptScopeInterceptor(),new MapF2FInterceptor()}; sessionFactory.setPlugins(interceptor); return sessionFactory.getObject(); }
DataPrepareEncryptScopeInterceptor,DataEncryptScopeInterceptor,RestoreDataScopeInterceptor,QueryDecryptScopeInterceptor 加密解密相关的插件,为什么这么配置,后面再来分析
// http://localhost:8502/api/test1 @RequestMapping("/test1") public String test1() throws Exception { UserPhone userPhone = new UserPhone(); userPhone.setUserNameEn("18488888888"); userPhone.setRealNameEn("张三"); userPhone.setIdNumberEn("483898329832983"); userPhone.setUserNo("89329832"); userPhone.setCnlCode("chenn"); userPhone.setChannelCode("3232"); userPhone.setType(1); userPhoneDao.insertUserPhone(userPhone); return "sucess"; }
在代码中,我没有对任何字段加密,但打印的sql中却体现了加密效果。再来看看数据库保存的数据怎样。
相应的字段被加密保存到数据库中。
// http://localhost:8502/api/test2 @RequestMapping("/test2") public String test2() throws Exception { UserPhone userPhone = userPhoneDao.selectUserPhoneByUserNameEn("18488888888"); System.out.println(JSON.toJSONString(userPhone)); return "sucess"; }
结果输出
从图中可以得知,在代码中输入的查询条件为18488888888,但sql打印出的查询条件已经被加密,在数据库中数据是被加密的,但是通过JSON.toJSONString(userPhone) JSON 打印,数据是被解密的。是不是感觉几招下来,开发功底尽显。不吹牛逼了,继续来看问题。
从上面两个小例子中,当插入数据时,能对数据库字段加密,当查询数据时,能对符合加密条件的字段加密,对不需要加密的字段则不会加密,大家不要有一种错觉,【我对所有的查询条件都加密,显然不是】,当然后面还会分析更新,批量更新,分页查询等遇到的问题及解决方案。
如果我们能精准的知道哪些查询条件字段需要被加密,当然还包括子查询 ,这必须涉及到一个问题,就是SQL解析,我们在mybatis插件中能得到如下SQL : SELECT * FROM lt_user_phone WHERE is_delete = 0 AND user_name_en = ? LIMIT 1
其实我们只需要知道SQL中每个 ? 所代表的是哪个字段,并且这个字段属于哪张表,即可与我们配置信息(配置了哪张表的哪些字段需要被加密解密)中的字段和表比对,如果与配置表中配置的表名和字段名相匹配,则表示这个字段是需要被加密或解密的,从而对mybatis中传入数据进行加密即可。 既然有了想法,那么接下来看如何做。
我们需要准备一个SQL解析包,能解析出每条SQL中?是哪个字段的条件并且属于哪张表?说是这么说,但怎么实现呢? SQL又有子查询,又有关联查询,又有子查询套子查询,我的天呐,不可能实现的嘛,这明显是写一个SQL解析器嘛。
当然,感谢互联网时代,感谢阿里,他提供了一个druid的包,使得项目得到进展 , 当然我个人是非常喜欢和崇拜阿里的,他提供了太多的开发框架,如 Druid ,Dubbo,Rocketmq,Arthas,Nacos,Sentinel等,我觉得这些框架都值得去研究,回归正题。
因为Druid包本身提供了对SQL的解析功能,只需要将这个包改成我们想要的为止,也忘记了曾经3天没日没夜的修改,最后比较完善,包路径为https://github.com/quyixiao/sql-parser.git
既然如此,我们来看包的执行效果。
public class TestSql2 { public static void printSql(String sql) { // 新建 MySQL Parser SQLStatementParser parser = new MySqlStatementParser(sql); SQLExprParser exprParser = parser.getExprParser(); Lexer lexer = exprParser.getLexer(); SQLObjectImpl.lexer = lexer; // 使用Parser解析生成AST,这里SQLStatement就是AST SQLStatement statement = parser.parseStatement(); // 使用visitor来访问AST MySqlSchemaStatVisitor visitor = new MySqlSchemaStatVisitor(); statement.accept(visitor); // 从visitor中拿出你所关注的信息 Collectionlist = visitor.getColumns(); List header = new ArrayList | () {{ add(new Cell("tableName")); add(new Cell("column")); add(new Cell("select")); add(new Cell("where")); add(new Cell("join")); add(new Cell("groupBy")); add(new Cell("having")); add(new Cell("fullName")); add(new Cell("dataType")); add(new Cell("pos")); add(new Cell("test")); }}; List | > body = new ArrayList
>(); for (TableStat.Column column : list) { List
cells = new ArrayList | (); cells.add(new Cell(column.getTable())); cells.add(new Cell(column.getName())); cells.add(new Cell(column.isSelect() + "")); cells.add(new Cell(column.isWhere() + "")); cells.add(new Cell(column.isJoin() + "")); cells.add(new Cell(column.isGroupBy() + "")); cells.add(new Cell(column.isHaving() + "")); cells.add(new Cell(column.getFullName() + "")); cells.add(new Cell(column.getDataType() + "")); cells.add(new Cell(column.getPos() + "")); if (column.getPos() > 10) { cells.add(new Cell(sql.substring(column.getPos(), column.getPos() + column.getName().length()))); } else { cells.add(new Cell("")); } body.add(cells); } String a = new ConsoleTable.ConsoleTableBuilder().addHeaders(header).addRows(body).build().toString(); System.out.println(a); } @org.junit.Test public void test() { String sql = "select * from lt_user_phone up left join lt_stage_borrow sb on up.unique_code = sb.unique_code where up.user_name = ? and up.real_name = ? and sb.status = ? "; System.out.println(sql); printSql(sql); } } |
sql为select * from lt_user_phone up left join lt_stage_borrow sb on up.unique_code = sb.unique_code where up.user_name = ? and up.real_name = ? and sb.status = ? ,SQL的意思很简单吧, 通过lt_user_phone这张表的user_name和real_name关联查询
lt_stage_borrow表,并且lt_stage_borrow.status为查询条件,意思就是通过lt_user_phone的user_name和real_name字段及lt_stage_borrow表的status字段查询表的所有数据 。
从执行结果来看,是不是拿到了? 对应的表名和字段名,可能大家觉得这个是比较简单的sql ,我们再来看更加复杂的sql,执行效果 。
我们再次写了一条复杂的sql , 其中传入了5个查询条件,毫无疑问,结果是正确的,当年我在测试这个包的时候,测试了几百条SQL,最终将发现的问题都修复了,而且现在已经在1000万用户以上的平台线上运行了2年左右,所以这款插件的准确性还是不用担心的,目前没有发现什么问题, 放心使用即可。
关于sql-parser插件的源码感兴趣可以自行去研究,当然感兴趣建议去研究Druid源码,里面涉及到SQL 的解析,当然不要想得有什么捷径,和其他语言解析器一样,都是一个字符一个字符的读取,最终构建成一颗语法树,当然呐,Druid插件的源码解析也并非一朝一夕阳能说清楚,也不是一篇博客能说明白,先放着,以后有机会再来写相关的博客 。
@Slf4j @Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})}) public class DataQueryEncryptScopeInterceptor extends SqlParserHandler implements Interceptor { public DataQueryEncryptScopeInterceptor() { } @Override public Object intercept(Invocation invocation) throws Throwable { StatementHandler statementHandler = (StatementHandler) PluginUtils.realTarget(invocation.getTarget()); MetaObject metaObject = SystemMetaObject.forObject(statementHandler); // 先判断是不是SELECT操作 BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql"); MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement"); if (SqlCommandType.SELECT.equals(mappedStatement.getSqlCommandType())) { //非select语句 this.sqlParser(metaObject); ParameterHandler parameterHandler = (ParameterHandler) metaObject.getValue("delegate.parameterHandler"); String mapperdId = PSqlParseUtil.getMapperId(mappedStatement); Configuration configuration = mappedStatement.getConfiguration(); encrySqlParamData(configuration, parameterHandler, mapperdId, boundSql); Object result = invocation.proceed(); return result; } else { return invocation.proceed(); } } @Override public Object plugin(Object target) { if (target instanceof StatementHandler) { //使用MyBatis提供的Plugin类生成代理对象 return Plugin.wrap(target, this); } return target; } @Override public void setProperties(Properties properties) { } }
其他代码都是mybatis 的常规操作,而encrySqlParamData代码就是我们查询条件加密的方法 。
public void encrySqlParamData(Configuration configuration, ParameterHandler parameterHandler, String mapperdId, BoundSql boundSql) { PTuple2> data = null; String sql = boundSql.getSql(); // 不能每条sql都进行解析,对于相同的SQL // 只做一次解析,将解析的结果以 mapperdId + md5(sql) 组成key // 存储到缓存中 String key = mapperdId + PMD5Util.encode(sql); try { PPTuple pluginTuple = enterMap.get(key); if (pluginTuple == null) { // data 为元组 // 第一个对象标识SQL中有没有需要被加密解密的字段 // 第二个对象为一个map , key 为 加密字段出现的位置,value 为字段名称 // 如select * from user where status = ? and user_name_en = ? // 而status 不需要被加密解密 ,user_name_en 需要被加密解密 // 则map 中存储的为 key = 1 , value = user_name_en data = getChangeColumn(configuration, mapperdId, sql, key).getData(); } else { data = pluginTuple.getData(); } // 这里使用了元组,虽然Java 使用元组不太好用 // 那还是用一下吧, first存储的是这条SQL有没有字段需要被加密解密 // 如果first 为true,表示SQL中有字段需要被加密,如果为false // SQL 中没有任何字段需要被加密解密 if (!data.getFirst()) { return; } Map paramMaps = data.getSecond(); Object parameterObject = boundSql.getParameterObject(); StringBuffer sb = new StringBuffer(); List parameterMappings = boundSql.getParameterMappings(); if (parameterMappings.size() > 0 && parameterObject != null) { // 如果parameterObject 是字符串,并且 // 并且 parameterMappings 只有一个参数 if (parameterMappings != null && parameterMappings.size() == 1 && parameterObject != null && parameterObject instanceof String) { // 通过反射修改 parameterObject 属性值 // 为加密后的值 Object encode = encode(parameterObject + ""); setFieldValue(parameterHandler, "parameterObject", encode); sb.append("(").append(parameterObject).append(" -> ").append(encode).append("),"); } else { MetaObject metaObject = configuration.newMetaObject(parameterObject); if (parameterObject instanceof DefaultSqlSession.StrictMap) { //表示一个集合 Set keySet = ((DefaultSqlSession.StrictMap>) parameterObject).keySet(); // 对于查询条件是一个list 或 collection时 // 需要将list或collection对象的值给替换掉,这个和mybatis源码相关 if(keySet.contains("list")){ List
因为上面的代码涉及到很多mybatis源码相关的知识,在之前的mybatis源码系中 深入浅出MyBatis技术原理与实战-学习-源码解析-MyBatis 运行原理(四) 详细的分析了mybatis 运行原理相关的知识,感兴趣可以去读一下, 这里就不再深入分析mybatis源码了,为了方便大家理解代码,我们通过例子的方式来分析。
所有的业务逻辑都在上面这些代码中了, 但是理解起来还是很抽象的,paramMaps 到底存储的是什么东西呢? 我们来看一个例子。
// http://localhost:8502/api/test7 @RequestMapping("/test7") public String test7() throws Exception { String [] arrays = new String[]{"18411111111","18422222222","18433333333","18444444444"}; ListuserPhone = userPhoneDao.selectUserPhoneByArrayUserNameEnsAndRealName(arrays,"张三") ; System.out.println(JSON.toJSONString(userPhone)); return "sucess"; }
ListselectUserPhoneByArrayUserNameEnsAndRealName(@Param("arrays") String[] arrays,@Param("realNameEn") String realNameEn);
相信大家对上面写法再熟悉不过了
boundSql.getSql()为 select * from lt_user_phone where user_name_en in (?,?,?,?) and real_name_en = ? ,有5个问号
getChangeColumn()方法返回的map为{0:“user_name_en”,1:“user_name_en”,2:“user_name_en”,3:“user_name_en”,4:“real_name_en”}
上面有没有发现一个特点,对于普通对象,直接通过反射修改其值即可,但是对于 boundSql.hasAdditionalParameter(propertyName) 为true时,则需要调用boundSql.setAdditionalParameter(propertyName, encode); 来修改他的值,也就是对于 ParameterMapping的property=为__frch_item_0,__frch_item_1,__frch_item_2,__frch_item_3时,则需要修改__frch_item_0的值,不然不生效,为什么这么做呢?我们看一下mybatis的源码 。
DefaultParameterHandler 参数处理器中的setParameters方法中。
public void setParameters(PreparedStatement ps) { ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId()); ListparameterMappings = boundSql.getParameterMappings(); if (parameterMappings != null) { for (int i = 0; i < parameterMappings.size(); i++) { ParameterMapping parameterMapping = parameterMappings.get(i); if (parameterMapping.getMode() != ParameterMode.OUT) { Object value; String propertyName = parameterMapping.getProperty(); if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params value = boundSql.getAdditionalParameter(propertyName); } else if (parameterObject == null) { value = null; } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { value = parameterObject; } else { MetaObject metaObject = configuration.newMetaObject(parameterObject); value = metaObject.getValue(propertyName); } TypeHandler typeHandler = parameterMapping.getTypeHandler(); JdbcType jdbcType = parameterMapping.getJdbcType(); if (value == null && jdbcType == null) { jdbcType = configuration.getJdbcTypeForNull(); } try { typeHandler.setParameter(ps, i + 1, value, jdbcType); } catch (TypeException e) { throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e); } catch (SQLException e) { throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e); } } } } }
因此boundSql.hasAdditionalParameter(propertyName)为true时,需要调用boundSql.setAdditionalParameter(propertyName, encode); 查询条件才起作用。 只有在最后才从 metaObject.getValue(propertyName); 获取属性值,可能大家觉得这有点断章取义,但是没有办法 ,mybatis源码就是这样写的 。大家可能感兴趣paramMaps的获取很巧妙嘛,完全迎合了mybatis源码,那又是怎样实现的呢?
public PPTuple getChangeColumn(Configuration configuration, String mapperdId, String sql, String key) { PPTuple pluginTuple = enterMap.get(key); if (pluginTuple != null) { return pluginTuple; } log.info(" 没有从缓存中获取数据,直接生成数据,key= " + key + ", sql = " + sql); // 如果sql = ? ,则直接返回 // 在mapper.xml 中 直接 通过 ${sql} 来执行sql ,对于这种情况 // 不做加密解密处理 if ("?".equals(sql.trim())) { pluginTuple = new PPTuple(false, null); enterMap.put(key, pluginTuple); log.info("sql是问号 : " + mapperdId + "sql : " + sql + ",key = " + key + ",pluginTuple = " + JSON.toJSONString(pluginTuple)); return pluginTuple; } MaplinkedHashMap = new LinkedHashMap<>(); PTuple2 , List > data = PSqlParseUtil.getParserInfo(sql).getData(); Collection columns = data.getFirst(); // 当sql 中的问号和 sql-parser框架解析出来的问号个数不相等 // 并且sql 中不包含in 时,打印提示信息,其实也没有什么用,用来找问题方便 if (columns != null) { int count = PStringUtils.getStringCountKey(sql, "?"); String message = " 解析sql得到的个?数为 " + columns.size() + ",sql中的?个数为 " + count; if (count != columns.size() && PStringUtils.getStringCountKey(sql.toLowerCase(), "in") <= 0) { log.info("message = " + message + " ,mappId = " + mapperdId + ", sql = " + sql + ",key=" + key); } } List tableNames = data.getSecond(); Map tableConfig = EncryptTableConfig.tableConfig; int i = 0; boolean canCache = true; for (TableStat.Column column : columns) { String tableName = PSqlParseUtil.getRealName(column.getTable()); String columnName = PSqlParseUtil.getRealName(column.getName()); // 如sql 为 select * from lt_user_phone where user_name_en in (?,?,?,?) and real_name_en = ? // 解析出表名 和 列名为 // lt_user_phone user_name_en // lt_user_phone user_name_en // lt_user_phone user_name_en // lt_user_phone user_name_en // lt_user_phone real_name_en // 如果表名和列名为我们所配置需要加密的表字段 // 则将结果加入到linkedHashMap中 if (tableConfig.containsKey(tableName)) { if (PSqlParseUtil.notEqQuestionMark(columnName)) { // PTuple2 , List
> columnNameData = tableConfig.get(tableName).getData(); List columnNames = columnNameData.getFirst(); if (columnNames.contains(columnName)) { linkedHashMap.put(i, columnName); } } // 这里涉及到另外一种考虑 // select * from lt_user_phone up left join lt_borrow b on up.id = b.user_id where up.user_name_en = ? and status = ? // 当lt_user_phone表没有status字段时 ,但lt_borrow 表有status字段时 // 这样写sql 是不会有问题,解析status是就不知道属于哪张表了 // 需要通过sql每一张表的所有字段,判断其是否有status字段,如果有 // 则这个字段属于这张表 } else if (PSqlParseUtil.isUnkown(tableName) && PSqlParseUtil.notEqQuestionMark(columnName)) { //如果表名为UNKOWN,但column 不为空时 int flag = 0; for (String realTableName : tableNames) { if (tableConfig.containsKey(realTableName)) { PTuple2 , List
> tableInfos = getTableInfo(configuration, realTableName).getData(); List tableColumns = tableInfos.getSecond(); PTuple2 , List
> columnNameData = tableConfig.get(realTableName).getData(); List columnNames = columnNameData.getFirst(); for (String tableColumn : columnNames) { if (tableColumns.contains(tableColumn) && tableColumn.equals(columnName)) { flag++; } } } } if (flag == 1) { linkedHashMap.put(i, columnName); } else { canCache = false; } } i++; } boolean isEncrypt = linkedHashMap.size() > 0; pluginTuple = new PPTuple(linkedHashMap.size() > 0, linkedHashMap); if (canCache) { enterMap.put(key, pluginTuple); log.info(" 加密信息获取 mapperdId : " + mapperdId + " , isEncrypt : " + isEncrypt + ",columns :" + JSON.toJSONString(pluginTuple) + ",key=" + key); } else { log.info(" 不加入缓存加密数据 mapperdId : " + mapperdId + " , isEncrypt : " + isEncrypt + ",columns :" + JSON.toJSONString(pluginTuple) + ",key=" + key); } return pluginTuple; }
对于第二种情况,当lt_user_phone 没有status字段,但lt_borrow有status字段,sql : select * from lt_user_phone up left join lt_borrow b on up.id = b.user_id where up.user_name_en = ? and status = ? ,这样写执行sql是没有问题的,但是在解析时会出现status不知道属于哪张表。
因此对于这种情况需要动态的遍历所有的表,拿到每张表的字段,再进行比对,如果匹配上了,这个字段肯定属于这张表 。 因此写了下面的sql进行查询表中的所有字段
SELECT COLUMN_NAME columnName, DATA_TYPE dataType, COLUMN_COMMENT columnComment,COLUMN_KEY columnKey FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = ‘" + tableName + "’,当然这条sql最好是加上数据库名,这样会更加准确。
为什么需要对parameterObject为DefaultSqlSession.StrictMap做单独的处理呢?看一下mybatis源码 。
这段代码的意思是什么呢? 我们在写查询条件时,如果传入的是一个List对象或一个数组,我们在Mapper层方法参数可以不加@Param注解,而在Mapper.xml 的 foreach循环中 collection 可以写collection或list,如果是数组的话,可以写成 collection=“array”,先来看collection="list"的例子。
// http://localhost:8502/api/test4 @RequestMapping("/test4") public String test4() throws Exception { ListuserPhones = new ArrayList<>(); UserPhone userPhone = new UserPhone(); userPhone.setUserNameEn("18488888888"); userPhones.add(userPhone); UserPhone userPhone1 = new UserPhone(); userPhone1.setUserNameEn("18488888889"); userPhones.add(userPhone1); List userPhoneList = userPhoneDao.selectUserPhoneByUserNameEnEntitys(userPhones); System.out.println(JSON.toJSONString(userPhoneList)); return "sucess"; }
ListselectUserPhoneByUserNameEnEntitys(List list);
再来看一个例子,如果传入的是数组时,该怎样处理呢?
// http://localhost:8502/api/test6 @RequestMapping("/test6") public String test6() throws Exception { UserPhone userPhone = new UserPhone(); userPhone.setUserNameEn("18488888888"); UserPhone userPhone1 = new UserPhone(); userPhone1.setUserNameEn("18488888889"); UserPhone [] arrays = new UserPhone[]{userPhone,userPhone1}; ListuserPhoneList = userPhoneDao.selectUserPhoneByUserNameArrayEnEntitys(arrays); System.out.println(JSON.toJSONString(userPhoneList)); return "sucess"; }
ListselectUserPhoneByUserNameArrayEnEntitys(UserPhone[] arraysxxx);
我们再来看另外一种情况,大家注意,这里打印的并不是查询出结果的值,而是请求参数的值
// http://localhost:8502/api/test8 @RequestMapping("/test8") public String test8() throws Exception { UserPhone userPhone = new UserPhone(); userPhone.setUserNameEn("18488888888"); UserPhone data = userPhoneDao.selectUserPhoneByUsernNameObject(userPhone); System.out.println(JSON.toJSONString(userPhone)); return "success"; }
UserPhone selectUserPhoneByUsernNameObject(UserPhone userPhone);
看下面结果,如果使用userPhone作为查询条件,调用Mapper 方法之后,再在代码中使用,发现竟然userNameEn被加密了
这显然不是我们想要的,我们的目的就是保证在代码中,一定是明文,而如果新来的开发小伙伴不小心使用了加密后的数据,带来不必要的麻烦 ,怎么办呢? 是怎样加密的,那就怎样解密回来不就好了不?这就是RestoreQueryDataScopeInterceptor实现类的由来
@Slf4j @Intercepts({@Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class})}) public class RestoreQueryDataScopeInterceptor extends SqlParserHandler implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { StatementHandler statementHandler = (StatementHandler) PluginUtils.realTarget(invocation.getTarget()); MetaObject metaObject = SystemMetaObject.forObject(statementHandler); this.sqlParser(metaObject); // 先判断是不是SELECT操作 BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql"); MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement"); String mapperdId = PSqlParseUtil.getMapperId(mappedStatement); Configuration configuration = mappedStatement.getConfiguration(); Object result = invocation.proceed(); try { decodeData(configuration, mapperdId, boundSql); } catch (Exception e) { log.error("查询解析失败", e); } return result; } ... }
发现没有,加密和解密插件的区别就是时机问题。
public interface StatementHandler { Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException; void parameterize(Statement statement) throws SQLException; void batch(Statement statement) throws SQLException; int update(Statement statement) throws SQLException;List query(Statement statement, ResultHandler resultHandler) throws SQLException; Cursor queryCursor(Statement statement) throws SQLException; BoundSql getBoundSql(); ParameterHandler getParameterHandler(); }
我们在prepare,parameterize,方法调用时,对方法参数进行加密处理,在query方法时将加密参数解密回来,因为此时Statement对象已经准备好了, 将加密参数修改回来对查询结果无影响,因此我们选择在query拦截器中将之前加密的参数解密回来 ,看看解密方法如何实现。
public void decodeData(Configuration configuration, String mapperdId, BoundSql boundSql) { PTuple2> data = null; String sql = boundSql.getSql(); String key = mapperdId + PMD5Util.encode(sql); try { PPTuple pluginTuple = enterMap.get(key); if (pluginTuple == null) { data = getChangeColumn(configuration, mapperdId, sql, key).getData(); } else { data = pluginTuple.getData(); } if (!data.getFirst()) { return; } Map paramMaps = data.getSecond(); Object parameterObject = boundSql.getParameterObject(); List parameterMappings = boundSql.getParameterMappings(); if (parameterMappings.size() > 0 && parameterObject != null) { MetaObject metaObject = configuration.newMetaObject(parameterObject); if (parameterObject instanceof DefaultSqlSession.StrictMap) { //表示一个集合 Set keySet = ((DefaultSqlSession.StrictMap>) parameterObject).keySet(); if(keySet.contains("list")){ List list = (List ) metaObject.getValue("list"); if (list != null && list.size() > 0) { Object object = list.get(0); if (!(object instanceof String)) { List collection = (List ) metaObject.getValue("collection"); int i = 0 ; for (Map.Entry paramMap : paramMaps.entrySet()) { MetaObject meta1 = configuration.newMetaObject(list.get(i)); MetaObject meta2 = configuration.newMetaObject(collection.get(i)); String propertyName = PSqlParseUtil.field2JavaCode(paramMap.getValue()); Object temp = meta1.getValue(propertyName); Object value = decode(temp); meta1.setValue(propertyName, value); meta2.setValue(propertyName, value); i ++; } } } } else { Object[] array = (Object[]) metaObject.getValue("array"); if (array != null && array.length > 0) { Object object = array[0]; if (!(object instanceof String)) { int i = 0 ; for (Map.Entry paramMap : paramMaps.entrySet()) { MetaObject meta1 = configuration.newMetaObject(array[i]); String propertyName = PSqlParseUtil.field2JavaCode(paramMap.getValue()); Object temp = meta1.getValue(propertyName); Object value = decode(temp); meta1.setValue(propertyName, value); i ++; } } } } } else { for (Map.Entry paramMap : paramMaps.entrySet()) { int index = paramMap.getKey(); ParameterMapping parameterMapping = parameterMappings.get(index); String propertyName = parameterMapping.getProperty(); PTuple2 indetValue = getIndexObject(boundSql, metaObject, propertyName, index).getData(); Object decode = decode(indetValue.getSecond()); if (decode != null) { metaObject.setValue(propertyName, decode); } if (indetValue.getFirst()) { boundSql.setAdditionalParameter(propertyName, decode); } } } } } catch (Exception e) { try { log.error("showSql exception " + JSON.toJSONString(data) + ",key = " + key + ",sql = " + sql + ",mapperId" + mapperdId, e); } catch (Exception ex) { log.error("decodeData exception", e); } } }
聪明的读者肯定会发现,解密方法和加密方法的实现原理一样,只是将原来的encode()方法改成decode()方法即可。
接下来,我们继续来看更新方法的实现。先将实现好的功能恢复回去 。
恢复成之前的版本,看只有DataQueryEncryptScopeInterceptor拦截器时会有什么问题。
从执行结果上来看,显然更新成功了, 是不是DataQueryEncryptScopeInterceptor 拦截器就可以了呢?为什么还要写一个DataQueryEncryptScopeInterceptor 来拦截更新条件的参数呢?
那批量更新有问题吗?我们继续看例子。
// http://localhost:8502/api/test10 @RequestMapping("/test10") public String test10() throws Exception { UserPhone userPhone1 = userPhoneDao.selectUserPhoneById(1l); UserPhone userPhone2 = userPhoneDao.selectUserPhoneById(2l); userPhone1.setUserNameEn("111111111111"); userPhone2.setUserNameEn("222222222222"); ListuserPhones = new ArrayList<>(); userPhones.add(userPhone1); userPhones.add(userPhone2); userPhoneService.updateBatchById(userPhones); UserPhone userPhone11 = userPhoneDao.selectUserPhoneById(1l); UserPhone userPhone22 = userPhoneDao.selectUserPhoneById(2l); System.out.println(JSON.toJSONString(userPhone11)); System.out.println(JSON.toJSONString(userPhone22)); return "success"; }
上面的例子也很简单,查询id = 1 和 2 的userPhone ,修改其user_name_en的值,进行批量更新
从上图结果来看,第一条更新语句进行了加密解密,但第二条更新语句,要加密的字段并没有加密成功, 为什么呢?难道是两条数据导致的吗?如果批量更新3条数据,会怎样呢?
现在发现规率了,如果批量更新,只有第一条数据会被加密,从第二条开始,后面的数据都没有被加密,那是什么原因导致这样的问题呢?
又发挥我在代码中寻寻觅觅的长处了,找到了批量处理代码 。
public int doUpdate(MappedStatement ms, Object parameterObject) throws SQLException { final Configuration configuration = ms.getConfiguration(); final StatementHandler handler = configuration.newStatementHandler(this, ms, parameterObject, RowBounds.DEFAULT, null, null); final BoundSql boundSql = handler.getBoundSql(); final String sql = boundSql.getSql(); final Statement stmt; // 如果SQL一样,mybatis为了优化性能,就会走下面代码 if (sql.equals(currentSql) && ms.equals(currentStatement)) { int last = statementList.size() - 1; stmt = statementList.get(last); applyTransactionTimeout(stmt); handler.parameterize(stmt);//fix Issues 322 BatchResult batchResult = batchResultList.get(last); batchResult.addParameterObject(parameterObject); } else { Connection connection = getConnection(ms.getStatementLog()); stmt = handler.prepare(connection, transaction.getTimeout()); handler.parameterize(stmt); //fix Issues 322 currentSql = sql; currentStatement = ms; statementList.add(stmt); batchResultList.add(new BatchResult(ms, sql, parameterObject)); } // handler.parameterize(stmt); handler.batch(stmt); return BATCH_UPDATE_RETURN_VALUE; }
从上述代码来看,mybatis为了优化性能,在更新处理时,如果两条SQL一样,则只会调用一次handler.prepare()方法,但无论什么情况parameterize()总是被调用,我们只要写一个对parameterize方法拦截的拦截器即可。
批量更新进入doUpdate()方法
批量更新数据都已经加密了
那只需要DataUpdateEncryptScopeInterceptor即可,还需要DataQueryEncryptScopeInterceptor拦截器做什么呢?我们继续来看问题。
我们写一个分页查询
// http://localhost:8502/api/test11 @RequestMapping("/test11") public String test11() throws Exception { Listusernames = new ArrayList<>(); usernames.add("111111111111"); usernames.add("222222222222"); usernames.add("333333333333"); PageUtils pageUtils = PageUtils.startPage(1, 10).doSelect(new ISelect() { @Override public List doSelect(IPage page) { return userPhoneDao.selectUserPhoneByPageUserNameEns(usernames); } }); System.out.println(JSON.toJSONString(pageUtils)); return "success"; }
PageUtils 为一个分页工具类,我不喜欢mybatis-plus用来做分页,太死板了,又不喜欢Github PageHelper那种,直接设置分页参数,再写一行查询语句 ,就帮你实现了分页,感觉两者之间没有太大关系,因此我自己写了一个分页工具类,做了一层包装,感觉这样更加贴切 。我们重点讲分页问题。
很明显,查询条件是做了加密的,但是遗憾的是总页数和总条数却为0 。
显然这样是不符合业务的,前端分页怎么能没有总页数呢?但出现这个问题的原因是什么呢?我们进入分页拦截器。
发现问题没有,并不是分页拦截器没有用,而是查询时,查询条件并没有被加密 ,不知道细心的读者有没有发现导致问题产生的原因 。
发现没有,分页插件是对prepare拦截,而我们的加密解密插件是对parameterize方法拦截,从执行流程来看,prepare()方法先于parameterize()方法执行,因此为了解决这个问题,需要将查询和更新插件分开,保证查询加密插件比分页插件先执行即可。因此DataQueryEncryptScopeInterceptor和DataUpdateEncryptScopeInterceptor一个都不能少, DataQueryEncryptScopeInterceptor主要对prepare方法拦截 ,只加密查询参数 ,而DataUpdateEncryptScopeInterceptor主要对parameterize方法拦截 , 只加密更新参数,修改后再来看分页结果 。
分页查询总条数时,查询条件已经加密 。
再来看查询结果
总条数和总页数已经有数值了。
对于查询和更新已经分析完了,那么查询结果怎样解密呢?
@Slf4j @Intercepts({@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})}) public class QueryDecryptScopeInterceptor implements Interceptor { public static MapoutMap = new ConcurrentHashMap<>(256); /** * 代替拦截对象的方法内容 * 责任链对象 */ @Override public Object intercept(Invocation invocation) throws Throwable { DefaultResultSetHandler defaultResultSetHandler = (DefaultResultSetHandler) PluginUtils.realTarget(invocation.getTarget()); //MappedStatement维护了一条 MetaObject metaObject = SystemMetaObject.forObject(defaultResultSetHandler); // 先判断是不是SELECT操作 MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("mappedStatement"); //获取当前resutType的类型 Object object = realTarget(invocation.getArgs()[0]); com.mysql.jdbc.Field[] fields = null; //BoundSql对象是处理SQL语句用的 MetaObject metaObjectResult = SystemMetaObject.forObject(object); if (object != null) { if (object instanceof JDBC42PreparedStatement) { fields = (com.mysql.jdbc.Field[]) metaObjectResult.getValue("results.fields"); } else if ("DruidPooledPreparedStatement".equals(object.getClass().getSimpleName())) { Object obx = metaObjectResult.getValue("stmt"); if (obx != null && "PreparedStatementProxyImpl".equals(obx.getClass().getSimpleName())) { fields = (com.mysql.jdbc.Field[]) metaObjectResult.getValue("stmt.statement.results.fields"); } else { fields = (com.mysql.jdbc.Field[]) metaObjectResult.getValue("stmt.results.fields"); } } else { log.info("查询没有捕捉到的类:" + object.getClass().getSimpleName()); } } Object result = invocation.proceed(); String mapperdId = PSqlParseUtil.getMapperId(mappedStatement); BoundSql boundSql = (BoundSql) metaObject.getValue("boundSql"); try { if (result != null) { PTuple2 > data = getChangeColumn(fields, mapperdId, boundSql.getSql()).getData(); if (data.getFirst()) { if (result instanceof Collection) { forceChageList((Collection) result, data.getSecond()); } else if (result.getClass().isArray()) { //如果是数组类型 forceChageList(Arrays.asList((Object[]) result), data.getSecond()); } else { forceChange(result, data.getSecond()); } } } } catch (Exception e) { log.error("sql select 解密数据异常 exception ", e); PDingDingUtils.sendText("异常编号 =" + Logger.inheritableThreadLocalNo.get() + "\n mapperId =" + mapperdId + "\n sql = " + boundSql.getSql() + "\n 异常堆栈 = " + PExceptionUtils.dealException(e)); } return result; } }
上面代码看上去一大堆,其实只要看懂JDBC就明白了,其实解密很简单。
无论是什么框架,总能拿到查询结果的表名和列名,这个和mybatis无关,jdbc的返回结果中也有这些数据 。
看一个例子
@Test public void jdbcQuery() { Connection conn = null; PreparedStatement pstemt = null; try { //注册加载jdbc驱动 Class.forName("com.mysql.jdbc.Driver"); //打开连接 conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/lz_test?characterEncoding=utf-8", "ldd_biz", "123456"); //创建执行对象 String sql = "select * from lz_test_user where id = ?"; pstemt = conn.prepareStatement(sql); pstemt.setObject(1,"14"); //执行sql语句 ResultSet rs = pstemt.executeQuery(); System.out.println("---------"); //展开结果集 while (rs.next()) { System.out.println(rs.getString("username")); } rs.close(); conn.close(); } catch (Exception e) { e.printStackTrace(); } }
因此我们都不需要sql-parser框架解析sql ,只需要将表名和列名与我们配置 需要加密的 字段相比对,如果是,则进行解密即可。原理很简单,就不深入分析。
当然有人会想,你这是单表查询,如果级联查询,子查询,这些也能从jdbc中看到查询出结果对应的表和列字段吗?当然可以。
修改sql select * from lt_stage_borrow sb left join lt_user_phone up on sb.unique_code = up.unique_code ,查看结果。
我相信细心的读者还发现一个问题,就是我还写了一个MapF2FInterceptor,这个插件有什么用呢?同样这个插件的源码很简单,不分析 ,只看应用场景,这个应用场景也是对之前的一篇博客的补充吧。 之前哪篇博客呢?2.mybatis实用小插件 ,主要对mybatis 的一些写法的优化,讲一种应用场景。
// http://localhost:8502/api/test12 @RequestMapping("/test12") public String test12() throws Exception { Listborrows = stageBorrowDao.selectStageBorrowByAll(); // 此时此刻 需要拿到所有用户 的用户名 List uniqueCodes = borrows.stream().map(StageBorrow::getUniqueCode).collect(Collectors.toList()); List userPhones = userPhoneDao.selectUserPhoneByUniqueCodes(uniqueCodes); Map mapUserPhone = userPhones.stream().collect(Collectors.toMap(UserPhone::getUniqueCode , Function.identity())); for(StageBorrow stageBorrow :borrows){ UserPhone userPhone = mapUserPhone.get(stageBorrow.getUniqueCode()); System.out.println("=========" + userPhone.getRealNameEn()); } System.out.println("==================还有一种写法========================="); for(StageBorrow stageBorrow :borrows){ // 这种法写,性能太差了 UserPhone userPhone = userPhoneDao.selectUserPhoneByUniqueCode(stageBorrow.getUniqueCode()); System.out.println("=========" + userPhone.getRealNameEn()); } return "success"; }
上面这个例子,想通过借款表查找所有借款用户的真实姓名,通过uniqueCode关联,上面提供了两种写法,
第一种
这种方式我不太喜欢,至少写了3行代码,同时我也不喜欢lamb表达式,因为没有python语法简洁,因此我基本不用。第二种写法,就不用说了,性能太差,java 就没有一种好的写法吗?很遗憾的告诉你,到少我没有发现,既然没有,那就自己想办法实现吧。
// http://localhost:8502/api/test13 @RequestMapping("/test13") public String test13() throws Exception { Listborrows = stageBorrowDao.selectStageBorrowByAll(); // 此时此刻 需要拿到所有用户 的用户名 Map mapUserPhone = userPhoneDao.selectUserPhoneMapByUniqueCode(borrows); for(StageBorrow stageBorrow :borrows){ UserPhone userPhone = mapUserPhone.get(stageBorrow.getUniqueCode()); System.out.println("=========" + userPhone.getRealNameEn()); } return "success"; }
先来看看效果
Mapper.xml的实现如下
@Bean2Map(key = "uniqueCode",value = "this") MapselectUserPhoneMapByUniqueCode(@IN @Row("uniqueCode") List uniqueCode);
Bean2Map注解有两个参数,key返回对象中的哪个字段封装为key , value 如果为"this" ,表示对象自身,如果为其他的字符串,则对对象中的某个属性, 方法参数中有两个注解,@IN注解可以传一个参数,参数对应的是下面加粗的条件 ,如果不传,则取方法名转化为下划线 unique_code ,
select * from lt_user_phone where unique_code in ( ? ,? ,?)
uniqueCode转化为下划线后为unique_code, @Row()注解也需要传一个参数,如这个参数表示是取StageBorrow对象的哪个属性作为in 的条件 ,本例中取StageBorrow的 uniqueCode属性作为in 的条件 。所以真正的sql为
SELECT * FROM lt_user_phone WHERE is_delete = 0 AND unique_code IN ( ‘1111’ , ‘2222’ , ‘3333’ , ‘1111’ )
接下来,我们继续看,如果返回的Map 的value是一个具体的值,而不是一个对象,那怎么办呢?
看Mapper中的写法
@Bean2Map(key = "uniqueCode",value = "realNameEn") MapString, String> selectUserPhoneMapByUniqueCodeRealName(@IN("uniqueCode") @Row("uniqueCode")ListStageBorrow> borrows);
以UserPhone对象的uniqueCode为key , realNameEn属性为value的Map ,同时@IN()注解的参数为加粗代码
select * from lt_user_phone where unique_code in ( ? ,? ,?),而 @Row(“uniqueCode”) 注解里的参数为in (?,?,?)里的条件 。
我们再回到test13方法。
应该加上空判断,如果userPhone为空,是不是导致userPhone.getRealNameEn()报空指针异常,其实你也不用担心 ,底层框架已经实现。我们修改lt_stage_borrow的unique_code的值 。
访问http://localhost:8502/api/test13 ,发现没有报空指针异常
@Bean2Map(key = "uniqueCode",value = "this",fillNull = false) MapString, UserPhone> selectUserPhoneMapByUniqueCode(@IN @Row("uniqueCode") ListStageBorrow> uniqueCode);
在@Bean2Map中设置fillNull参数为false。
当然你不用担心,fillNull的默认值为true,如果你不想让插件帮你填充空值时,可以设置fillNull = false。
hibernate虽然我不喜欢用,但是他有些功能我觉得还是很好的。比如级联拉取。
这个是什么意思呢?先来看看Mapper
ListStageBorrowDto> selectStageBorrowByAllDto();
@Data public class StageBorrowDto extends StageBorrow{ @Pull(self = "uniqueCode", target = "uniqueCode", sort = "desc",limit = 1 ) private ListuserPhoneList; }
Pull注解的self表示StageBorrow的字段, target 表示UserPhone对应的字段,相当于写一条SQL
select * from lt_user_phone where unique_code = #{uniqueCode} order by id desc limit 1
其实selectStageBorrowByAllDto()的意图就是,在查询StageBorrow 时将与之关联的UserPhone对象拉取出来 。当然,拉取的UserPhone也可以是一个对象,并不一定是一个list。
// http://localhost:8502/api/test16 @RequestMapping("/test16") public String test16() throws Exception { ListStageBorrowDto2> borrows = stageBorrowDao.selectStageBorrowByAllDto2(); for(StageBorrowDto2 stageBorrowDto : borrows){ UserPhone userPhone = stageBorrowDto.getUserPhone(); System.out.println(JSON.toJSONString(userPhone)); } return "success"; }
@Data public class StageBorrowDto2 extends StageBorrow{ @Pull(self = "uniqueCode", target = "uniqueCode", sort = "desc",limit = 1 ) private UserPhone userPhone; }
Mapper
ListselectStageBorrowByAllDto2();
当然此时此刻还有人会想,如果我想在lt_user_phone表拉取时,加一个条件怎么办呢?那我们也可以来终级大招。 修改StageBorrowDto3实体
@Data public class StageBorrowDto3 extends StageBorrow{ @Pull(self = "uniqueCode", target = "uniqueCode", where = " type = 1 and cnl_code = 'chenn' ", sort = "desc",limit = 1 ) private UserPhone userPhone; }
当然,有人想,我也分不清self ,target 也容易弄混,能不能都写到where条件里,那当然可以。
@Data public class StageBorrowDto4 extends StageBorrow{ @Pull(where = " unique_code = #{uniqueCode} and type = 1 and cnl_code = 'chenn' ", sort = "desc",limit = 1 ) private UserPhone userPhone; }
当然,有人还 会想,如果UserPhone对象内部还想拉取其他对象,能不能实现呢?当然可以啦。
依然还是先创建两个实体,StageBorrowDto5包含了UserPhoneDto 。 主要意思是什么呢?我想通过借款表查找到用户表,再将用户所有的登陆日志拉取出来 。
@Data public class StageBorrowDto5 extends StageBorrow{ @Pull(where = " unique_code = #{uniqueCode} and type = 1 and cnl_code = 'chenn' ", sort = "desc",limit = 1 ) private UserPhoneDto userPhone; } @Data public class UserPhoneDto extends UserPhone { @Pull(where = " user_id = #{id} ", sort = "desc") private ListuserLoginList; }
上面的pull功能依然有点小问题,如果是需要被加密解密字段就不能用pull进行拉取了,需要自己重新写Mapper,这一点需要注意 。
文章到这里也告一段落了,整个项目的开发过程中,非常感谢涛哥,华坤,小谷,中正,铮俊,志轩 提供了问题及保贵的建议,才得以框架在生产上正常运行。
同时感谢技术大佬爱姐毫无条件的支持与信任, 感谢测试团队黎甜,进惠,海燕,潘含,闪哥辛勤不辍的不断发现问题,如数据被加密两遍,分页问题,批量更新问题,通过条件查询过后,查询条件被加密问题等 ,最终这些问题都得以解决,这个框架是一个技术团队花了几个月的时间共同努力的结果。希望其中的解决方案对同行有所帮助 。
我觉得,如果是一个构架师,应该根据开发的需要开发更加易用,出bug率少的工具给开发人员使用,让开发人员投身到具体的业务中去,而少写无用的东西,我也不是什么架构师,只是一个技术爱好者而已 。
如果网友有什么好的建议及更好的方案提供,请给我留言吧,下一篇博客见。
本文相关项目的github地址为
https://github.com/quyixiao/lz_mybatis_plugin.git
https://github.com/quyixiao/github-lz-mybatis-en-plugins.git
https://github.com/quyixiao/spring-data-en-decoder
https://github.com/quyixiao/sql-parser.git