Mybatis 使用与深入解析

Mybatis 使用

解决问题思路

  • 查看异常栈,是否可以从异常描述找到问题原因
  • 如果异常描述找不到具体原因,可以尝试定位报错的源码行,断点debug查看报错原因

排查经历 之 类型转换

  • 接口
List<PrmPromotionEntity> getPromotionOverViewList(String tenantCode, Date startTime, Date endTime);
  • XML
  <select id="getPromotionOverViewList" resultMap="BaseResultMap" parameterType="java.lang.Integer" >
    select
    <include refid="Base_Column_List" />
    from PRM_PROMOTION p
    where <![CDATA[ p.start_time  > #{startTime, jdbcType = TIMESTAMP} ]]>
    <![CDATA[ and p.end_time <= #{endTime, jdbcType = TIMESTAMP} ]]>
    and p.tenant_code = #{tenantCode, jdbcType=VARCHAR}
    and p.lifecycle = 'PUBLISH'
  </select>
  • ERROR 日志
    在这里插入图片描述
    错误日志很明显:java.util.Date cannot be cast to java.lang.Integer,但是怎么解决确不知道。
    如果对Mybatis源码比较清楚,这一堆东西是可以解决问题的,至少能知道它是在说什么。

Could not set parameters for mapping: ParameterMapping{property=‘startTime’, mode=IN, javaType=class java.lang.Integer, jdbcType=TIMESTAMP, numericScale=null, resultMapId=‘null’, jdbcTypeName=‘null’, expression=‘null’}.

很明显,我是没看懂,所以选择debug去了。将以下几行代码依次打上断点。
在这里插入图片描述
分析会得知,入参:startTime,javaType 是 Integer,但是jdbcType是TIMESTAMP,所以我看了一下入参和xml,发现我入参是String tenantCode, Date startTime, Date endTime,但是xml设置是parameterType=“java.lang.Integer”,Mybatis会将三个入参都当Integer来解析。
尝试去掉parameterType=“java.lang.Integer”,Mybatis会将三个参数当做object解析,报错解决。

  • 尝试不是用xml,用Mybatis注解
    在这里插入图片描述
  • ERROR日志
    在这里插入图片描述
    一看,Sql有问题,理论上不应该有问题呀,对比了一下,注解和xml的SQL。哦哦哦,面试中常见的# 和 $ 符号的问题,我这里用的$,是直接替换的,所以 canadaGoose_001连引号都没有。改成# 就可以了。
  • 那么,到此就没有问题了么?跑一下发现还有问题,debug日志看一下,发现数据有的,但是封装java对象的时候只有id是有值的。很明显咯,数据库字段名用的下划线分割而java用的驼峰法。
    怎么解决呢?没有去百度,因为可以确定是结果解析有问题,同时可以确定有下划线转驼峰法逻辑,所以ResultHandler相关的类打了一些断点,开发分析,在DefaultResultSetHandler中找到以下代码:

    final String property = metaObject.findProperty(propertyName, configuration.isMapUnderscoreToCamelCase());

所以咯!设置isMapUnderscoreToCamelCase为true就可以了。去官网找,
在这里插入图片描述当然官网还有很多其他配置。https://mybatis.org/mybatis-3/zh/configuration.html

  • 使用的java配置
@Configuration
public class MyBatisConfig {
    @Bean
    ConfigurationCustomizer mybatisConfigurationCustomizer() {
        return configuration -> configuration.setMapUnderscoreToCamelCase(true);
    }
}

排查经历 之 时间类型转换

Mybatis 深入解析

SqlSessionFactoryBuilder

在这里插入图片描述

SqlSessionFactory

在这里插入图片描述
可以看出,DefaultSessionFactory暴露出的接口具体实现是openSessionFromConnection和openSessionFromDataSource 两个方法。
在这里插入图片描述
openSessionFromConnection和openSessionFromDataSource 两个方法区别在于事务的开启参数和自动提交参数来源不一样。
一个是与connection保持一致,一个是与数据库连接信息中保持一致。
会发现一个问题,openSession方法中没有太多复杂操作。
配置、执行器以及是否自动提交,下面我们重点看一下Executor。

Executor

在这里插入图片描述

创建执行器
缓存包装
安装插件

可以看出Executor有三种基本类型:简单、复用和批处理
另外还有一个包装类CachingExecutor.
几种执行器的作用显而易见,那么这些都是怎么实现的呢?让我们继续往下看。
在这里插入图片描述
在这里插入图片描述
无论是哪个都继承了BaseExecutor,而BaseExecutor实现了一些通用操作。

结构
SimpleExecutor在这里插入图片描述
ReuseExecutor在这里插入图片描述
BatchExecutor在这里插入图片描述
  • Executor 中枢作用
    从Executor的入参就可以看出,Executor搜集了执行一个SQL的所有条件,如:MappedStatement【statement信息】,parameter【参数】,rowBounds【返回数据条数限制】,ResultHandler【结果集处理】,BoundSql【执行的SQL】
获取StatementHandler
获取Statement
执行Statement
通过statement获取ResultSetWrapper
ResultHandler处理ResultSetWrapper

其实,此刻,基本流程已经完了。但是中间有很多细节,前面各个参数已经初始化好,Executor只是调用各模块实现,下面可以逐个分析。

  • 另外,对比一下SimpleExecutor 和 ReuseExecutor 的 doUpdate 和 doQuery 方法。
    在这里插入图片描述
    SimpleExecutor 在doUpdate 和doQuery执行后,加了closeStatement。

    根据经验猜测,

    • SimpleExecutor每次调用都会创建一个Statement,用完立刻close;
    • ReuseExecutor会复用Statement。

    而获取Statement方式都是使用的prepareStatement方法。
    继续撸代码,你会发现,StatementHandler有两个实现类,进哪个类呢? BaseStatementHandler,因为RoutingStatementHandler是包装类,具体相关请看Statement章节。
    在这里插入图片描述

  • 其实,看着这里已经蒙了,继续看doUpdate,doQuery的具体实现,发现会涉及到StatementHandler,BoundSql等不清楚干了些什么。先看一下Statement。

StatementHandler

包装 : RoutingStatementHandler
SimpleStatementHandler
BaseStatementHandler
PreparedStatementHandler
CallableStatementHandler
StatementHandler

发现StatementHandler其实也只是对JDBC的Statement获取做了一些封装。

  • instantiateStatement:Statement实例化,不同handler实现创建Statement参数不一样,可以看一下JDBC的Connection创建Statement接口。

  • query、update方法封装实现也比较简单

      SimpleStatementHandler:
      @Override
      public void batch(Statement statement) throws SQLException {
        String sql = boundSql.getSql();
        statement.addBatch(sql);
      }
    
      @Override
      public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
        String sql = boundSql.getSql();
        statement.execute(sql);
        return resultSetHandler.handleResultSets(statement);
      }
    

    理解:StatementHandler 简化JDBC的statement操作,并做了一个简单分类。

ParameterHandler

在这里插入图片描述
不难看出,ParameterHandler是提供给CallableStatement和PreparedStatement使用的。
且,实现只有DefaultParameterHandler一个。那么DefaultParameterHandler.setParameters都干了什么呢?

-- DefaultParameterHandler:
public void setParameters(PreparedStatement ps) {
    ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
    List<ParameterMapping> parameterMappings = 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 | SQLException e) {
            throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
          }
        }
      }
    }
  }

ResultSetHandler

Plugin 结合实际场景分析

项目中的包结构依赖

在这里插入图片描述
在这里插入图片描述

实力打脸了。IDEA的《External Libraries》 包含了项目中所有的模块的jar,如果各个模块独立运行,即使使用的jar包版本不一样,也没有问题,恰巧,我们现在就是这样,有一个模块开发,使用了tkMybatis,没有影响其他模块,因为其他模块没有引用此模块。从项目管理角度,这种问题是不可以发生的,应该将依赖放入dependencyManager中管理。

Plugin 实践到原理

0 使用Mybatis插件实现审计字段自动注入。

@Component
@Intercepts({
        @Signature(
                type = Executor.class,
                method = "update",
                args = {MappedStatement.class, Object.class}),
        @Signature(
                type = StatementHandler.class,
                method = "parameterize",
                args = {Statement.class})})
public class MybatisEntityPluginInterceptor implements Interceptor{
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        try {
            Object[] args = invocation.getArgs();
            if (args[0] instanceof MappedStatement) {
                // 映射的各种信息,SQL信息、接口方法对应的参数、接口方法的全名称等等
                MappedStatement mappedStatement = (MappedStatement) args[0];
                // 获取执行语句的类型
                SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
                if (args[1] instanceof HashMap<?, ?>) {
                    HashMap<?, ?> hashMap = (HashMap<?, ?>) args[1];
                    // 这里分别处理每个参数的实体注入
                    // mappedStatement.getId()   -> 获取方法的全路径; 例如: cn.xxx.xxx.xxx.ClassName.methodName
                    String id = mappedStatement.getId();
                    Optional<Method> methodInMapperOpt = getMethodInMapper(id);
                    if(!methodInMapperOpt.isPresent()){
                        return invocation.proceed();
                    }
                    String[] paramKeys = parseParamKeysByMethod(methodInMapperOpt.get());
                    for (String key : paramKeys) {
                        Object o = hashMap.get(key);
                        if (o instanceof List) {
                            List<?> list = (List<?>) o;
                            for (Object entity : list) {
                                Field[] allAuditFields = FieldAccess.get(entity.getClass()).getFields();
//                               Field[] allAuditFields = MyReflectUtils.getAllAuditFields(entity.getClass());
                                setAuditField(sqlCommandType, entity, allAuditFields);
                            }
                        } else if (o instanceof BaseEntity) {
                            // 这里处理不是集合的情况,通过继承 cn.dmahz.entity.Base,可证明为Java Bean
                            Field[] allAuditFields = FieldAccess.get(o.getClass()).getFields();
//                           Field[] allAuditFields = MyReflectUtils.getAllAuditFields(o.getClass());
                            setAuditField(sqlCommandType, o, allAuditFields);
                        }
                    }
                } else if (args[1] instanceof BaseEntity) {
                    Object o = args[1];
                    // 这里处理不是集合的情况,通过继承 cn.dmahz.entity.Base,可证明为Java Bean
                    Field[] allAuditFields = FieldAccess.get(o.getClass()).getFields();
//                   Field[] allAuditFields = MyReflectUtils.getAllAuditFields(o.getClass());
                    setAuditField(sqlCommandType, o, allAuditFields);
                }
            }
        } catch (Exception e) {
            log.warn("Generator Audit Field has error.", e);
        }

        // 让拦截器继续处理剩余的操作
        return invocation.proceed();
    }

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

    @Override
    public void setProperties(Properties properties) {
    }

    private Optional<Method> getMethodInMapper(String id) throws ClassNotFoundException {
        String className = id.substring(0, id.lastIndexOf("."));
        String methodName = id.substring(id.lastIndexOf("."));
        return Arrays.stream(Class.forName(className).getMethods()).filter(one -> one.getName().equals(methodName)).findFirst();
    }

    /**
     * 解析方法的形参的key值,key值用于在 ParamMap中查找值,进行填充审计字段
     */
    private String[] parseParamKeysByMethod(Method method) {
        ArrayList<String> keyList = new ArrayList<>();
        Parameter[] parameters = method.getParameters();
        for (Parameter parameter : parameters) {
            Param parameterAnnotation = parameter.getAnnotation(Param.class);
            if (parameterAnnotation != null) {
                keyList.add(parameterAnnotation.value());
            } else {
                // 形参名称
                String name = parameter.getName();
                // 类型的简写名称
//                String simpleName = parameter.getType().getSimpleName();
                if (StringUtils.isNotBlank(name)) {
                    keyList.add(name);
                }
            }
        }
        return keyList.toArray(new String[0]);
    }

    /**
     * 包装一下重复的代码,方便调用
     */
    private void setAuditField(SqlCommandType sqlCommandType, Object o, Field[] fields) throws IllegalAccessException {
        for (Field field : fields) {
            setAuditField(sqlCommandType, o, field);
        }
    }

    /**
     * 简化,创建、更新时间由数据库自动操作,创建人、修改人简化为:操作人(operator)
     */
    private void setAuditField(SqlCommandType sqlCommandType, Object o, Field field) throws IllegalAccessException {
        if ((sqlCommandType == SqlCommandType.INSERT || sqlCommandType == SqlCommandType.UPDATE)
                && field.isAnnotationPresent(AutoOperator.class)) {
            String currentCode;
            try {
                currentCode = CommonBizUtil.getCurrentUserCode();
            } catch (Exception e) {
                //这里仅作测试,忽略空指针异常
                currentCode = "system"; //TODO: 非Web环境,当前用户ID测试值(创建值),这里暂时使用默认值:system
            }
            field.setAccessible(true);
            field.set(o, currentCode);
        }
//        if (sqlCommandType == SqlCommandType.INSERT) {
//            if (field.isAnnotationPresent(CreatedBy.class)) {
//                String currentUserId;
//                try {
//                    currentUserId = CommonUtil.getCurrentUserCode();
//                } catch (NullPointerException e) {
//                    //这里仅作测试,忽略空指针异常
//                    currentUserId = "非Web环境,当前用户ID测试值(创建值)";
//                }
//                field.set(o, currentUserId);
//            } else if (field.isAnnotationPresent(CreatedDate.class)) {
//                field.set(o, System.currentTimeMillis());
//            } else if (field.isAnnotationPresent(Id.class)) {
//                String uuId = UUID.randomUUID().toString();
//                field.set(o, uuId.replace("-", ""));
//            }
//        } else if (sqlCommandType == SqlCommandType.UPDATE) {
//            if (field.isAnnotationPresent(LastModifiedBy.class)) {
//                String currentUserId;
//                try {
//                    currentUserId = CommonUtil.getCurrentUserCode();
//                } catch (NullPointerException e) {
//                    //这里仅作测试,忽略空指针异常
//                    currentUserId = "非Web环境,当前用户ID测试值(更新值)";
//                }
//                field.set(o, currentUserId);
//            } else if (field.isAnnotationPresent(LastModifiedDate.class)) {
//                field.set(o, System.currentTimeMillis());
//            }
//        }
    }
}

大致看了一下,
1 Mybatis 配置类Configuration 维护一个interceptorChain,各个入口将Interceptor实现类添加到interceptorChain中
在这里插入图片描述
1.1 Mybatis自身通过XML解析处Interceptor实现类
1.2 Spring通过注解解析
1.3 而PageHelper 则是通过自动装载自动添加,不错的思路

在这里插入图片描述
2 我们在创建四大对象(ParameterHandler、ResultSetHandler、StatementHandler、Executor)的时候,会通过InterceptorChain依次遍历插件,调用插件的plugin方法。
在这里插入图片描述
2 -> 3 通常plugin方法实现是这样的,将插件自己嵌入四大对象中。

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

3 Plugin 代码大致是一个代理,贴出来先。

public class Plugin implements InvocationHandler {
  private final Object target;
  private final Interceptor interceptor;
  private final Map<Class<?>, Set<Method>> signatureMap;
  private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
    this.target = target;
    this.interceptor = interceptor;
    this.signatureMap = signatureMap;
  }
  public static Object wrap(Object target, Interceptor interceptor) {
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    Class<?> type = target.getClass();
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    if (interfaces.length > 0) {
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
    return target;
  }
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      Set<Method> methods = signatureMap.get(method.getDeclaringClass());
      if (methods != null && methods.contains(method)) {
        return interceptor.intercept(new Invocation(target, method, args));
      }
      return method.invoke(target, args);
    } catch (Exception e) {
      throw ExceptionUtil.unwrapThrowable(e);
    }
  }
  private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
    Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
    // issue #251
    if (interceptsAnnotation == null) {
      throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
    }
    Signature[] sigs = interceptsAnnotation.value();
    Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();
    for (Signature sig : sigs) {
      Set<Method> methods = signatureMap.computeIfAbsent(sig.type(), k -> new HashSet<>());
      try {
        Method method = sig.type().getMethod(sig.method(), sig.args());
        methods.add(method);
      } catch (NoSuchMethodException e) {
        throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
      }
    }
    return signatureMap;
  }
  private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
    Set<Class<?>> interfaces = new HashSet<>();
    while (type != null) {
      for (Class<?> c : type.getInterfaces()) {
        if (signatureMap.containsKey(c)) {
          interfaces.add(c);
        }
      }
      type = type.getSuperclass();
    }
    return interfaces.toArray(new Class<?>[interfaces.size()]);
  }
}

4

Spring 集成Mybatis

在上一节Plugin有提到,Spring通过注解解析添加Interceptor,那么,这块是怎么实现的呢?
1.mybatis-spring-boot-autoconfigure-2.0.1.jar 包装MybatisAutoConfiguration类会ObjectProvider加载Interceptor的SpringBean的数据。
2.mybatis-spring-2.0.1.jar包中SqlSessionFactoryBean implements FactoryBean, InitializingBean, ApplicationListener 通过afterPropertiesSet构造sqlSessionFactory,此间会遍历第一步加载的Interceptor,调用addInterceptor方法。
通过MapperScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware 实现动态注册Mybatis的相关的Mapper类

总结

架构

主流程
DB
StatementHandler
Executor
SqlSession
SqlSessionFactory
Configuration
映射参数输入
映射参数输出

基本缓存

  • PerpetualCache使用HashMap实现

装饰器

  • BlockingCache:阻塞包装器
    从缓存中getObject时,首先回去尝试获取锁,当一个线程getObject时,没有从缓存中拿到值,就会一直持有锁,直到
    putObject将锁释放;这个保证了多个线程同时访问且缓存没有时,只需要一个线程A去访问数据库,其他线程阻塞,直到A线程从数据库拿到数据并维护到缓存中。
  @Override
  public void putObject(Object key, Object value) {
    try {
      delegate.putObject(key, value);
    } finally {
      releaseLock(key);
    }
  }
  @Override
  public Object getObject(Object key) {
    acquireLock(key);
    Object value = delegate.getObject(key);
    if (value != null) {
      releaseLock(key);
    }
    return value;
  }
  • FifoCache:缓存大小为1024的先进先出队列包装器
    可以看一下基本缓存PerpetualCache类,只是对HashMap进行了简单的包装,没有大小限制。如果长时间运行,缓存的数据会越来越多,导致内存溢出,FifoCache包装类就是为了解决以上问题。程序启动时,delegate和keyList都为空,随着时间推移,缓存的sql结果越来越多,当达到1024后,如果有第1025个SQL结果调用putObject方法,将会把第1个SQL结果删除,然后把1025入缓存,这样后续内存中将一直保持缓存1024个SQL结果。
  private final Cache delegate; // 被包装的缓存
  private final Deque<Object> keyList; // 队列
  private int size; // 缓存大小,默认1024
 @Override
 public void putObject(Object key, Object value) {
    cycleKeyList(key);
    delegate.putObject(key, value);
  }
 private void cycleKeyList(Object key) {
    keyList.addLast(key);
    if (keyList.size() > size) {
      Object oldestKey = keyList.removeFirst();
      delegate.removeObject(oldestKey);
    }
  }
  • LoggingCache:日志及统计
  private final Log log;
  private final Cache delegate;
  protected int requests = 0; // 请求次数
  protected int hits = 0; // 命中次数
    @Override
  public Object getObject(Object key) {
    requests++;
    final Object value = delegate.getObject(key);
    if (value != null) {
      hits++;
    }
    if (log.isDebugEnabled()) {
      log.debug("Cache Hit Ratio [" + getId() + "]: " + getHitRatio());  // 日志打印命中率
    }
    return value;
  }
  private double getHitRatio() {
    return (double) hits / (double) requests;
  }
  • LruCache:最近最少使用队列包装器
    没看懂,慢慢研究
/**
 *    Copyright 2009-2019 the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package org.apache.ibatis.cache.decorators;

import java.util.LinkedHashMap;
import java.util.Map;

import org.apache.ibatis.cache.Cache;

/**
 * Lru (least recently used) cache decorator.
 *
 * @author Clinton Begin
 */
public class LruCache implements Cache {

  private final Cache delegate;
  private Map<Object, Object> keyMap;
  private Object eldestKey;

  public LruCache(Cache delegate) {
    this.delegate = delegate;
    setSize(1024);
  }

  @Override
  public String getId() {
    return delegate.getId();
  }

  @Override
  public int getSize() {
    return delegate.getSize();
  }

  public void setSize(final int size) {
    keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
      private static final long serialVersionUID = 4267176411845948333L;

      @Override
      protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
        boolean tooBig = size() > size;
        if (tooBig) {
          eldestKey = eldest.getKey();
        }
        return tooBig;
      }
    };
  }

  @Override
  public void putObject(Object key, Object value) {
    delegate.putObject(key, value);
    cycleKeyList(key);
  }

  @Override
  public Object getObject(Object key) {
    keyMap.get(key); //touch
    return delegate.getObject(key);
  }

  @Override
  public Object removeObject(Object key) {
    return delegate.removeObject(key);
  }

  @Override
  public void clear() {
    delegate.clear();
    keyMap.clear();
  }

  private void cycleKeyList(Object key) {
    keyMap.put(key, key);
    if (eldestKey != null) {
      delegate.removeObject(eldestKey);
      eldestKey = null;
    }
  }

}

  • ScheduledCache:缓存超时包装器
    缓存超时控制,默认1小时。
  private final Cache delegate;
  protected long clearInterval;
  protected long lastClear;

  public ScheduledCache(Cache delegate) {
    this.delegate = delegate;
    this.clearInterval = TimeUnit.HOURS.toMillis(1);
    this.lastClear = System.currentTimeMillis();
  }
  private boolean clearWhenStale() {
    if (System.currentTimeMillis() - lastClear > clearInterval) {
      clear();
      return true;
    }
    return false;
  }


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值