今天在做项目中遇到了一个非常有意思的BUG,写下来分享一下,希望遇到相同问题的同学能更快的解决;这个BUG是这样的:
BUG1: 在项目启动运行后,用户第一次登录的时候,我根据用户输入的账号从数据库查找到用户对象User, 当用户登录成功后, 返回User对象,为了避免敏感数据,我将User进行隐藏密码处理,代码如下:
Admin admin = selectOne(new EntityWrapper<Admin>().eq("account", account));
if(admin==null){
throw new MyException("用户名不存在");
}
password = OftenTool.md5Encode(password);
System.out.println("用 户 数 据:"+admin);
System.out.println("用户输入密码:"+password);
System.out.println("数据库存密码:"+admin.getPassword());
if(!password.equals(admin.getPassword())){
throw new MyException("密码错误");
}
admin.setPassword(null);
return admin;
那么问题来了,第一次登录情况是正常的,但退出后,重新登录,控制台输出的admin其他信息正常,就是密码为空;所以就会一直报 密码错误 ,当然因为我使用了MyBatis的二级缓存,所以用户第二次登录时从缓存中拿取数据的;所以当输出密码为null的时候,我就知道是缓存出现了问题,然后,我又做了几次实验: 将代码
admin.setPassword(null);
去掉; 那样所有的登录就会正常.....;晕, Mapper 缓存不是缓存的数据库查出的那个对象吗? 怎么开始缓存我操作后的数据了,这已经与SQL没有关系了啊?
还有更奇葩的BUG:
BUG2: 这也是关于缓存的BUG,情况是这样的:
RepairOrder repairOrder = repairOrderMapper.selectById(repairStatus.getRepairOrderId());
if(repairOrder==null){
throw new MyException("订单不存在");
}
if(repairOrder.getCompanyId()!= repairStatus.getCompanyId()){
throw new MyException("参数错误");
}
第一次执行这段代码的时候是成功的,但是第二次执行的时候,就出错了,抛出了参数错误,当时我就凌乱了,明明一样的参数,第二次执行的时候就发生错误,当然有了上面问题的解决经验,就知道一定又是缓存在做怪了,然后开启DeBug模式,来在跑一遍,结果我更凌乱了: 上面显示,repairOrder.getCompanyId()为1, repairStatus.getCompanyId()也为1,但repairOrder.getCompanyId()!= repairStatus.getCompanyId()为true; 我靠, 1!=1 ==> 为true; (数据类型都是Integer), 又懵圈了; 不甘心的我有修改一下:
boolean b = repairOrder.getCompanyId()==repairStatus.getCompanyId();
if(!b){
throw new MyException("参数错误");
}
这样更直观一点,直接看变量b的值就好了, 结果在DeBug模式下 b竟然为 false, 但1==1 为 false, 晕;
我最后重新定义变量,才解决了问题:
RepairOrder repairOrder = repairOrderMapper.selectById(repairStatus.getRepairOrderId());
if(repairOrder==null){
throw new MyException("订单不存在");
}
int repairOrderCompanyId = repairOrder.getCompanyId();
int repairStatusCompanyId = repairStatus.getCompanyId();
boolean b = repairOrderCompanyId==repairStatusCompanyId;
if(!b){
throw new MyException("参数错误");
}
上面这两种情况截至现在依然不知道具体哪里出了问题,只知道是关于MyBatis缓存的问题;
平时一直都在用,而具体的实现原理却是不太清楚,那么到底是什么原因呢?就像第一个问题,我还专门写了一个测试,在各中环境下测试发现:
只有在Service实现层中查询使用的mapper会缓存操作后的对象(user1),即使你new一个新的对象(user2),将缓存中的对象直接赋值给新对象(user2=user1),然后操作新的对象(user2.setName(null)),那么他也会缓存对新对象的操作(user2); 除非采用get,set方法或构造器将user1中属性的值赋给user2中属性,则不会修改缓存;
所以你可以在需要的实体中创建一个这样的构造器:
public User (User user) {
this.age = user.getAge();
this.consumeIntegral = user.getConsumeIntegral();
this.creationTime = user.getCreationTime();
this.email = user.getEmail();
this.id = user.getId();
this.isAdmin = user.getIsAdmin();
this.nickname = user.getNickname();
this.phone = user.getPhone();
this.roleId = user.getRoleId();
this.sex = user.getSex();
this.state = user.getState();
this.surplusIntegral = user.getSurplusIntegral();
}
或者我们可以通过反射来复制对象:
package com.gy.demo.common.utils.object;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
/**
*@description TODO 复制对象工具类
*@date 2018年1月11日
*@author geYang
**/
public class CopyObject {
public static Object copyEntity(Object t) throws Exception{
//获取对象Class
Class<? extends Object> clazz = t.getClass();
//获取对象默认构造器
Constructor<? extends Object> constructor = clazz.getDeclaredConstructor(new Class[]{});
//创建复制对象
Object object = constructor.newInstance(new Object[]{});
//获取对象全部属性;
Field[] fields = clazz.getDeclaredFields();
for(Field field : fields){
//获取属性名称,类型
String fieldName = field.getName();
if("serialVersionUID".equals(fieldName)){
continue;
}
Class<?> type = field.getType();
//获取get,set方法名
String getMethodName;
String setMethodName;
if(type==boolean.class){
getMethodName = fieldName;
setMethodName = "set"+fieldName.substring(2);
} else {
getMethodName = "get"+fieldName.substring(0, 1).toUpperCase()+fieldName.substring(1);
setMethodName = "set"+fieldName.substring(0, 1).toUpperCase()+fieldName.substring(1);
}
//获取get,set方法
Method getMethod = clazz.getDeclaredMethod(getMethodName, new Class[]{});
Method setMethod = clazz.getDeclaredMethod(setMethodName, type);
//获取被复制对象的属性值
Object value = getMethod.invoke(t, new Object[]{});
//复制对象赋值
setMethod.invoke(object, new Object[]{value});
}
return object;
}
}
而在Controller中调用查询则不会缓存操作后的对象,会缓存直接查出来的对象;
而第二个问题原因还没有找到,就像灵异了一样;
终于BUG2的解决办法找到了, 具体代码如下:
//打印false
System.out.println(repairOrder.getCompanyId()==repairStatus.getCompanyId());
//打印true
System.out.println(repairOrder.getCompanyId().equals(repairStatus.getCompanyId()));
在这里既然都是用 Integer 对象来做比对,所以用 equals 函数来做具体判断更好, 而当做int来处理显然是不行的,所以在以后的编码中还需要注意注意在注意
所以有必要来认真学习一下MyBatis的缓存机制:
那么就来认真看一下MyBatis的文档: http://www.mybatis.org/mybatis-3/zh/configuration.html
MyBatis 包含一个强大的,可配置,可定制的缓存;
在MyBatis中缓存分为一级缓存(会话缓存)和二级缓存;
使用二级缓存时首先打开全局缓存开关: mybatis-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<!-- 全局映射器启用二级缓存 开关 -->
<setting name="cacheEnabled" value="true"/>
<!-- 延迟加载的全局开关. 当开启时,所有关联对象都会延迟加载. 特定关联关系中可通过设置fetchType属性来覆盖该项的开关状态. 默认false-->
<setting name="lazyLoadingEnabled" value="false"/>
<!-- 当开启时,任何方法的调用都会加载该对象的所有属性.否则,每个属性会按需加载(参考lazyLoadTriggerMethods). 默认false (true in ≤3.4.1)-->
<setting name="aggressiveLazyLoading" value="false"/>
<!-- 是否允许单一语句返回多结果集(需要兼容驱动). 默认 true -->
<setting name="multipleResultSetsEnabled" value="true"/>
<!-- 使用列标签代替列名.不同的驱动在这方面会有不同的表现, 具体可参考相关驱动文档或通过测试这两种不同的模式来观察所用驱动的结果. 默认true-->
<setting name="useColumnLabel" value="true"/>
<!-- 允许 JDBC 支持自动生成主键,需要驱动兼容. 如果设置为 true 则这个设置强制使用自动生成主键,尽管一些驱动不能兼容但仍可正常工作(比如 Derby) 默认false-->
<setting name="useGeneratedKeys" value="false"/>
<!-- 指定 MyBatis 应如何自动映射列到字段或属性. NONE 表示取消自动映射;PARTIAL 只会自动映射没有定义嵌套结果集映射的结果集. FULL 会自动映射任意复杂的结果集(无论是否嵌套). 默认 PARTIAL-->
<setting name="autoMappingBehavior" value="PARTIAL"/>
<!-- 指定发现自动映射目标未知列(或者未知属性类型)的行为. NONE: 不做任何反应; WARNING: 输出提醒日志 ('org.apache.ibatis.session.AutoMappingUnknownColumnBehavior' 的日志等级必须设置为 WARN); FAILING: 映射失败 . 默认NONE-->
<setting name="autoMappingUnknownColumnBehavior" value="NONE"/>
<!-- 配置默认的执行器, SIMPLE:就是普通的执行器; REUSE:执行器会重用预处理语句(prepared statements); BATCH:执行器将重用语句并执行批量更新, 默认SIMPLE-->
<setting name="defaultExecutorType" value="SIMPLE" />
<!-- 设置超时时间,它决定驱动等待数据库响应的秒数. -->
<setting name="defaultStatementTimeout" value="6000" />
<!-- 为驱动的结果集获取数量(fetchSize)设置一个提示值.此参数只可以在查询设置中被覆盖. -->
<setting name="defaultFetchSize" value="600" />
<!-- 允许在嵌套语句中使用分页(RowBounds).如果允许使用则设置为false. 默认false -->
<setting name="safeRowBoundsEnabled" value="false"/>
<!-- 允许在嵌套语句中使用分页(ResultHandler),如果允许使用则设置为false. 默认true -->
<setting name="safeResultHandlerEnabled" value="false"/>
<!-- 是否开启自动驼峰命名规则(camel case)映射,即从经典数据库列名 A_COLUMN 到经典 Java 属性名 aColumn 的类似映射. 默认:false-->
<setting name="mapUnderscoreToCamelCase" value="true"/>
<!-- MyBatis 利用本地缓存机制(Local Cache)防止循环引用(circular references)和加速重复嵌套查询. SESSION: 这种情况下会缓存一个会话中执行的所有查询.
STATEMENT: 本地会话仅用在语句执行上, 对相同 SqlSession 的不同调用将不会共享数据. 默认SESSION -->
<setting name="localCacheScope" value="SESSION"/>
<!-- 当没有为参数提供特定的 JDBC 类型时,为空值指定 JDBC 类型. 某些驱动需要指定列的 JDBC 类型,多数情况直接用一般类型即可,比如 NULL、VARCHAR 或 OTHER. 默认OTHER-->
<setting name="jdbcTypeForNull" value="OTHER"/>
<!-- 指定哪个对象的方法触发一次延迟加载. 默认 equals,clone,hashCode,toString -->
<setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
<!-- 指定动态 SQL 生成的默认语言. 默认 org.apache.ibatis.scripting.xmltags.XMLLanguageDriver -->
<setting name="defaultScriptingLanguage" value="org.apache.ibatis.scripting.xmltags.XMLLanguageDriver"/>
<!-- 指定默认枚举. 默认 org.apache.ibatis.type.EnumTypeHandler -->
<!-- <setting name="defaultEnumTypeHandler" value=""/> -->
<!-- 指定当结果集中值为 null 的时候是否调用映射对象的 setter(map 对象时为 put)方法,这对于有 Map.keySet()依赖或 null值初始化的时候是有用的.
注意基本类型(int,boolean等)是不能设置成 null 的 . 默认 false-->
<setting name="callSettersOnNulls" value="false"/>
<!-- 当返回行的所有列都是空时,MyBatis默认返回null. 当开启这个设置时,MyBatis会返回一个空实例.
请注意,它也适用于嵌套的结果集 (i.e. collectioin and association) 默认 false-->
<setting name="returnInstanceForEmptyRow" value="false"/>
<!-- 指定 MyBatis 增加到日志名称的前缀. -->
<setting name="logPrefix" value=""/>
<!-- 指定 MyBatis 所用日志的具体实现,未指定时将自动查找. SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING-->
<setting name="logImpl" value="SLF4J"/>
<!-- 指定VFS的实现.自定义VFS的实现的类全限定名,以逗号分隔. -->
<!-- <setting name="vfsImpl" value=""/> -->
<!-- 允许使用方法签名中的名称作为语句参数名称.为了使用该特性,你的工程必须采用Java8编译,并且加上-parameters选项.(从3.4.1开始) 默认 true -->
<setting name="useActualParamName" value="true"/>
<!-- 指定一个提供Configuration实例的类. 这个被返回的Configuration实例是用来加载被反序列化对象的懒加载属性值.
这个类必须包含一个签名方法static Configuration getConfiguration(). (从 3.2.3 版本开始) ,值: 类型别名或者全类名 -->
<!-- <setting name="configurationFactory" value=""/> -->
</settings>
</configuration>
1, 一级缓存是默认支持的,不过是作用于同一个sqlSession中,我这里就不做过多的说明,详细可参考: http://www.360doc.com/content/15/1205/07/29475794_518018352.shtml
2, 主要来看看二级缓存: 二级缓存在默认状态下是不会开启的,需要我们去设置,当然也是非常的简单:在SQL映射文件(*Mapper.xml)文件中,只需要加上:
<!-- 开启二级缓存 -->
<cache/>
它的作用如下:
� 所有在映射文件里的select语句都将被缓存;
� 所有在映射文件里insert,update和delete语句会清空缓存;
� 缓存使用 "最近很少使用" 算法来回收;
� 缓存不会被设定的时间所清空;
� 每个缓存可以存储1024个列表或对象的引用(不管查询出来的结果是什么);
� 缓存将作为 "读/写" 缓存,意味着获取的对象不是共享的且对调用者是安全的.不会有其它的调用者或线程潜在修改.
2, 缓存元素的所有特性都可以通过属性来修改:
<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
<!--
高级的配置创建一个FIFO缓存;
让60秒就清空一次;
存储512个对象结果或列表引用;
并且返回的结果是只读;
因此在不用的线程里的两个调用者修改它们可能会引用冲突
-->
<!--
清除规则如下:
� LRU- 最近最少使用法:移出最近较长周期内都没有被使用的对象;
� FIFO- 先进先出:移出队列里较早的对象;
� SOFT- 软引用:基于软引用规则,使用垃圾回收机制来移出对象;
� WEAK- 弱引用:基于弱引用规则,使用垃圾回收机制来强制性地移出对象;
� 默认值是LRU;
-->
<!--
flushInterval: 属性可以被设置为一个正整数,代表一个合理的毫秒总计时间.默认是不设置,因此使用无间隔清空即只能调用语句来清空;
size : 属性可以设置为一个正整数,你需要留意你要缓存的对象和你的内在环境,默认值是1024;
readOnly : 属性可以被设置为true或false.只读缓存将对所有调用者返回同一个实例.因此都不能被修改,这可以极大的提高性能.可写的缓存将通过序列化来返回一个缓存对象的拷贝.这会比较慢,但是比较安全.所以默认值是false
-->
注意: readOnly 只读属性,在我测试的过程中,根本没有发现什么区别,有没有好像都一样, 更新后都后重新查询, 上面的BUG1问题也同样会出现; 可能是我打开的方式不对, 因此很不理解文档中的 "不能被修改" 是什么意思.
来源:oschina
链接:https://my.oschina.net/u/3681868/blog/1605957