手敲Mybatis(16章)-一级缓存功能实现

1.实现目的

这一节的目的主要是实现SqlSession级别的缓存,也就是一级缓存,首先看下图一,用户可以通过设置来进行是否开启一级缓存,不设置的化默认开启一级缓存,localCacheScope=SESSION为要设置一级缓存,localCacheScope=STATEMENT为不要设置一级缓存,(所以后面在清理缓存时会进行判断,如果是STATEMENT就删除缓存)。

2.简单说明

然后我们就需要解析下缓存设置,拿到缓存级别,执行Sql语句前,将按Mybatis的规定处理缓存key(id,参数,Sql语句,环境等等,生成对应hash值),然后判断缓存中是否有当前key的结果数据,有的化结果数据直接返回,没有的话就去执行数据库查询,然后将查询的结果存储到一级缓存Map中,然后判断缓存级别是否是STATEMENT,是的话代表不进行缓存操作,那此时删除,下次进来还是继续查询库,不是的话就不删除,留着继续执行同一SqlSession会话使用。

其实不算难,还是很简单,也很有调理,这就是设计的魅力,把每个类与属性现实化,那对应需要改动哪里,就都很清晰。

3.XML图

XML类图就是对应上面所说的逻辑的实现。

1.XMLConfigBuilder里专门解析设置操作,Configuration则是缓存级别的存储,此时需要用到LocalCacheScope枚举类,

2.Executor类里query方法添加一个参数缓存Key参数(CacheKey),然后在BaseExecutor里用没有缓存key参数的quey方法来获取缓存key的操作(这里获取就是生成,将对应需要的参数传到CacheKey类里处理Hash),得到后调用有缓存Key参数的query方法,这时此方法根据当前key去缓存查询是否有数据,有的话缓存拿出,没有执行queryFromDatabase方法,从数据库获取,获取完毕存储缓存里(存储Cache接口PerpetualCache类的map里)

4.代码实现 

4.1 解析缓存设置

XMLConfigBuilder:解析配置XML构建器,这里需要添加私有方法settingsElement,主要解析缓存设置,解析完毕将缓存级别存储到configuration的LocalCacheScope枚举类下。

public class XMLConfigBuilder extends BaseBuilder {// 省略其他方法public Configuration parse() {// STEP-17 添加设置settingsElement(root.element("settings"));}/*** 解析配置在 XML 文件中的缓存机制。并把解析出来的内容存放到 Configuration 配置项中。* <settings>* <!--缓存级别:SESSION/STATEMENT-->* <setting name="localCacheScope" value="SESSION"/>* </settings>*/private void settingsElement(Element context) {if (context == null) return;List<Element> elements = context.elements();Properties props = new Properties();for (Element element : elements) {props.setProperty(element.attributeValue("name"), element.attributeValue("value"));}// 设置缓存级别configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty("localCacheScope")));}
}

Configuration:Configuration类里就针对缓存级别进行赋值操作。

public class Configuration {// 缓存机制,默认不配置的情况是 SESSIONprotected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;public LocalCacheScope getLocalCacheScope() {return localCacheScope;}public void setLocalCacheScope(LocalCacheScope localCacheScope) {this.localCacheScope = localCacheScope;}
}

LocalCacheScope:缓存级别枚举,

/*** @Author df* @Description: 本地缓存机制;* SESSION 默认值,缓存一个会话中执行的所有查询* STATEMENT 不支持使用一级缓存,本地会话仅用在语句执行上,对相同 SqlSession 的不同调用将不做数据共享* @Date 2024/1/9 10:10*/
// step 17添加
public enum LocalCacheScope {SESSION,STATEMENT
}

4.2 执行器缓存处理

Executor:

1.Executor类的修改是关于query方法时新添加缓存Key(CacheKey)操作,

2.添加了清除一级缓存方法定义以及创建缓存Key的方法定义

然后在BaseExecutor执行query时执行缓存存储,并根据参数生成缓存key,调用createCacheKey方法,增删改时删除缓存操作调用clearLocalCache方法。

public interface Executor {// step-17 添加CacheKey参数<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException;// step-17添加清理Session缓存void clearLocalCache();//  step-17添加创建缓存 KeyCacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);
}

BaseExecutor:

1.update方法:添加了调用此方法就会触发清除缓存操作clearLocalCache();,当然Mybaties的操作增删改全部调用update方法。

2.query定义的有两个方法,所以没有缓存Key的那个需要先生成缓存Key,传给有缓存Key参数的query方法。

3.有缓存Key参数的query方法,先判断queryStack是否为0,是否isFlushCacheRequired强制刷新如果都是则清除下当前缓存,然后根据缓存Key去调用一级缓存(PerpetualCache类)Map里是否存储了数据,没有的话去调用queryFromDatabase方法(查询库拿到数据结果),缓存有的话直接返回就可,最后判断下缓存级别是否STATEMENT,是的话删除缓存,不是的话不操作。

4.queryFromDatabase(),这是添加的私有方法,主要是查询库结果数据,查询到的结果存储到一级缓存中。

5.commit方法的更改就是事务提交就清除缓存,所以是SqlSession级别的吗,rollback方法也是调用此方法就清除缓存。

6.clearLocalCache方法,此方法清除一级缓存。

7.createCacheKey方法, 创建缓存Key的业务处理,按照MyBatis 对于其 Key 的生成采取规则为:[mappedStatementId + offset + limit + SQL + queryParams + environment]生成一个哈希码作为 Key 使用,所以依次把参数传给CacheKey的update的方法。

public abstract class BaseExecutor implements Executor {// 省略其他属性,方法    // 本地缓存protected PerpetualCache localCache;private boolean closed;// 查询堆栈protected int queryStack = 0;protected BaseExecutor(Configuration configuration, Transaction transaction) {this.configuration = configuration;this.transaction = transaction;this.wrapper = this;this.localCache = new PerpetualCache("LocalCache");}// update添加清除缓存操作@Overridepublic int update(MappedStatement ms, Object parameter) throws SQLException {// step-17添加-----------------------------startif (closed) {throw new RuntimeException("Executor was closed.");}clearLocalCache();// step-17添加------------------------------endreturn doUpdate(ms, parameter);}// query查询时创建缓存Key,将Mybatis规定的参数传入进去。@Overridepublic <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {BoundSql boundSql = ms.getBoundSql(parameter);// step-17 创建缓存Key,用这些参数组成缓存使用的Key。CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);return query(ms, parameter, rowBounds, resultHandler, key, boundSql);}@Overridepublic <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {// step-17添加if (closed) {throw new RuntimeException("Executor was closed.");}// 清理局部缓存,查询堆栈为0则清理。queryStack 避免递归调用清理if (queryStack == 0 && ms.isFlushCacheRequired()) {clearLocalCache();}List<E> list;try {queryStack++;// 根据cacheKey从localCache中拿到结果数据list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;if (list == null) {// 缓存没有拿到数据就去数据库查询下list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);}} finally {queryStack--;}if (queryStack == 0) {// 如果是STATEMENT证明不用缓存,所以此处清理缓存if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {clearLocalCache();}}return list;// step-17添加-----------end}// step-17添加,去数据库查询数据,查询结果将存储到一级缓存中private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {List<E> list;// 先存储占位符号localCache.putObject(key, ExecutionPlaceholder.EXECUTION_PLACEHOLDER);try {list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);} finally {// 删除占位符号localCache.removeObject(key);}// 将查询的数据存入当前缓存localCache.putObject(key, list);return list;}// 事务提交完毕清理缓存@Overridepublic void commit(boolean required) throws SQLException {if (closed) {throw new RuntimeException("Cannot commit, transaction is already closed");}// step-17添加clearLocalCache();if (required) {transaction.commit();}}// 事务回滚完毕清理缓存@Overridepublic void rollback(boolean required) throws SQLException {if (!closed) {// step-17添加try {clearLocalCache();} finally {if (required) {transaction.rollback();}}}}// step-17添加,清理一级缓存@Overridepublic void clearLocalCache() {if (!closed) {localCache.clear();}}/*** 创建缓存Key的hash,MyBatis 对于其 Key 的生成采取规则为:[mappedStatementId + offset + limit + SQL + queryParams + environment]生成一个哈希码作为 Key 使用。*/@Overridepublic CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {if (closed) {throw new RuntimeException("Executor was closed.");}CacheKey cacheKey = new CacheKey();// sql的idcacheKey.update(ms.getId());// offset cacheKey.update(rowBounds.getOffset());// limit cacheKey.update(rowBounds.getLimit());// SQL cacheKey.update(boundSql.getSql());List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();// 根据参数获取参数的值for (ParameterMapping parameterMapping : parameterMappings) {Object value;String propertyName = parameterMapping.getProperty();if (boundSql.hasAdditionalParameter(propertyName)) {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);}// queryParamscacheKey.update(value);}// 环境idif (configuration.getEnvironment() != null) {cacheKey.update(configuration.getEnvironment().getId());}return cacheKey;}
}

4.3 缓存操作

包:package cn.bugstack.mybatis.cache;

Cache接口:缓存的接口,这里提供了保存缓存、删除缓存、查询缓存、清除缓存、获取缓存长度等定义的方法。

/*** @Author df* @Description: SPI(Service Provider Interface) for cache providers. 缓存接口* 缓存接口主要提供了数据的存放、获取、删除、情况,以及数量大小的获取。这样的实现方式和我们通常做业务开发时,定义的数据存放都是相似的。* @Date 2024/1/9 10:24*/
// step 17添加
public interface Cache {/*** 获取ID,每个缓存都有唯一ID标识*/String getId();/*** 存入值*/void putObject(Object key, Object value);/*** 获取值*/Object getObject(Object key);/*** 删除值*/Object removeObject(Object key);/*** 清空*/void clear();/*** 获取缓存大小*/int getSize();
}

包名:package cn.bugstack.mybatis.cache.impl;

PerpetualCache:一级缓存类,实现Cache的接口,把缓存放入到Map中就可以其他操作拉。

/*** @Author df* @Description: 一级缓存,在 Session 生命周期内一直保持,每创建新的 OpenSession 都会创建一个缓存器 PerpetualCache,* 一级缓存实现类也叫永久缓存* @Date 2024/1/9 10:26*/
// step 17添加
public class PerpetualCache implements Cache {private Logger logger = LoggerFactory.getLogger(PerpetualCache.class);private String id;// 使用HashMap存放一级缓存数据,session 生命周期较短,正常情况下数据不会一直在缓存存放private Map<Object, Object> cache = new HashMap<>();public PerpetualCache(String id) {this.id = id;}@Overridepublic String getId() {return id;}@Overridepublic void putObject(Object key, Object value) {cache.put(key, value);}@Overridepublic Object getObject(Object key) {Object obj = cache.get(key);if (null != obj) {logger.info("一级缓存 \r\nkey:{} \r\nval:{}", key, JSON.toJSONString(obj));}return obj;}@Overridepublic Object removeObject(Object key) {return cache.remove(key);}@Overridepublic void clear() {cache.clear();}@Overridepublic int getSize() {return cache.size();}
}

CacheKey:缓存Key操作类,这个方法主要是BaseExecutor里的query方法调用,主要是doUpdate()方法,这里边的代码都有说明,这个方法主要是将传过来的参数处理成hash,并把原参数放入updateList(后边equal对比使用)。

这里重写了equals和hashcode等方式,如果遇到相同哈希值,避免对象重复,那么 CacheKey 缓存Key重写了 equals 对比方法。

/*** @Author df* @Description: 缓存 Key,一般缓存框架的数据结构基本上都是 Key->Value 方式存储* MyBatis 对于其 Key 的生成采取规则为:[mappedStatementId + offset + limit + SQL + queryParams + environment]生成一个哈希码* @Date 2024/1/9 10:34*/
// step 17添加
public class CacheKey implements Cloneable, Serializable {private static final long serialVersionUID = 1146682552656046210L;public static final CacheKey NULL_CACHE_KEY = new NullCacheKey();private static final int DEFAULT_MULTIPLYER = 37;private static final int DEFAULT_HASHCODE = 17;private int multiplier;private int hashcode;private long checksum;private int count;private List<Object> updateList;public CacheKey() {this.hashcode = DEFAULT_HASHCODE;this.multiplier = DEFAULT_MULTIPLYER;this.count = 0;this.updateList = new ArrayList<>();}public CacheKey(Object[] objects) {this();updateAll(objects);}public int getUpdateCount() {return updateList.size();}public void update(Object object) {if (object != null && object.getClass().isArray()) {int length = Array.getLength(object);for (int i = 0; i < length; i++) {Object element = Array.get(object, i);doUpdate(element);}} else {doUpdate(object);}}/*** 1.根据参数计算hash码值* 2.为了保证不重复处理计算最终的码值* 3.并将对象放入updateList集合中*/private void doUpdate(Object object) {// 确保hashcode一直都是有的。int baseHashCode = object == null ? 1 : object.hashCode();// 为了跟踪缓存更新的次数。count++;// 为了计算一个累积的校验和,用于检测缓存数据的一致性。checksum += baseHashCode;// 引入一个与更新次数相关的权重或因子,影响最终的哈希值。baseHashCode *= count;// 最终的哈希码值,相乘计算保证了对象或其属性变化时,哈希码都会改变hashcode = multiplier * hashcode * baseHashCode;// 目的是为了跟存储的参数进行对比updateList.add(object);}public void updateAll(Object[] objects) {for (Object o : objects) {update(o);}}/*** 如果遇到相同哈希值,避免对象重复,那么 CacheKey 缓存Key重写了 equals 对比方法。这也就为什么在 doUpdate* 计算哈希方法时,把对象添加到 updateList.add(object); 集合中,就是用于这里的 equal 判断使用。* */// 重写对象的equals方法,用于对象判断@Overridepublic boolean equals(Object object) {if (this == object) {return true;}if (!(object instanceof CacheKey)) {return false;}final CacheKey cacheKey = (CacheKey) object;if (hashcode != cacheKey.hashcode) {return false;}if (checksum != cacheKey.checksum) {return false;}if (count != cacheKey.count) {return false;}for (int i = 0; i < updateList.size(); i++) {Object thisObject = updateList.get(i);Object thatObject = cacheKey.updateList.get(i);if (thisObject == null) {if (thatObject != null) {return false;}} else {if (!thisObject.equals(thatObject)) {return false;}}}return true;}@Overridepublic int hashCode() {return hashcode;}// 将每个参数都以冒号形式拼接。@Overridepublic String toString() {StringBuilder returnValue = new StringBuilder().append(hashcode).append(':').append(checksum);for (Object obj : updateList) {returnValue.append(':').append(obj);}return returnValue.toString();}@Overridepublic CacheKey clone() throws CloneNotSupportedException {CacheKey clonedCacheKey = (CacheKey) super.clone();clonedCacheKey.updateList = new ArrayList<>(updateList);return clonedCacheKey;}
}

NullCacheKey:NULL值操作。

/*** @Author df* @Description: NULL值缓存Key* @Date 2024/1/9 10:36*/
// step 17添加
public class NullCacheKey extends CacheKey{private static final long  serialVersionUID = 3704229911977019465L;public NullCacheKey() {super();}
}

4.4 其他更改

为了能够兼容使用,其他地方的更改。

ExecutionPlaceholder:ExecutionPlaceholder类,这个是在存储结果数据集前先存储此占位符。

包名:package cn.bugstack.mybatis.executor;

/*** @Author df* @Description: 占位符* @Date 2024/1/9 11:29*/
public enum ExecutionPlaceholder {EXECUTION_PLACEHOLDER
}

MappedStatement:MappedStatement修改,此类添加私有的变量

public class MappedStatement {// step-17添加private boolean flushCacheRequired;public boolean isFlushCacheRequired() {return flushCacheRequired;}
}

DefaultSqlSession:此类selectList方法调用query方法时去掉Sql的参数。

public class DefaultSqlSession implements SqlSession {@Overridepublic <E> List<E> selectList(String statement, Object parameter) {logger.info("执行查询 statement:{} parameter:{}", statement, JSON.toJSONString(parameter));MappedStatement ms = configuration.getMappedStatement(statement);try {// step-17修改删除参数Sqlreturn executor.query(ms, parameter, RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER);} catch (SQLException e) {throw new RuntimeException("Error querying database.  Cause: " + e);}}
}

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/236255.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

char常见问题之一【C语言】

引出 在所写的代码中&#xff1a; char ch0 "asd";报错&#xff1a;因为char类型的变量只能存储一个字符&#xff0c;不能存储字符串 char ch1a;正确 char ch2"a";报错&#xff1a;因为&#xff0c;虽然a是一个字符&#xff0c;但是用了双引号&#xf…

大语言模型下载,huggingface和modelscope加速

huggingface 下载模型 如果服务器翻墙了&#xff0c;不用租机器 如果服务器没翻墙&#xff0c;可以建议使用下面的方式 可以租一台**autodl**不用显卡的机器&#xff0c;一小时只有1毛钱&#xff0c;启动学术加速&#xff0c;然后下载&#xff0c;下载完之后&#xff0c;用scp…

2024年CES展会都有些啥?亮点集锦都在这里

&#x1f4a1; 大家好&#xff0c;我是可夫小子&#xff0c;《小白玩转ChatGPT》专栏作者&#xff0c;关注AIGC、读书和自媒体。 CES在科技界是一场盛会&#xff0c;被誉为科技界的春晚&#xff0c;展会上前沿的技术、概念的产品吸引不少关注。2024年CES是在2023年大语言模型…

ChatGPT到底能做什么呢?

1、熟练掌握ChatGPT提示词技巧及各种应用方法&#xff0c;并成为工作中的助手。 2、通过案例掌握ChatGPT撰写、修改论文及工作报告&#xff0c;提供写作能力及优化工作 3、熟练掌握ChatGPT融合相关插件的应用&#xff0c;完成数据分析、编程以及深度学习等相关科研项目。 4、…

Ansible的切片特性与多机器选取

一、【概述】 本文介绍一下Ansible的多机器选取和切片特性&#xff0c;这个还是一个比较有用的技巧&#xff0c;可以快速选取仓库中我们需要的机器清单。 因为该特性可能与其他工具语法稍微有些不一样&#xff0c;时间长了会忘&#xff0c;值得记录一下 二、【具体说明】 1…

2024年如何使用WordPress构建克隆Udemy市场

您想创建像 Udemy 这样的学习管理 (LMS) 网站吗&#xff1f;最好的学习管理系统工具LifterLMS将帮助您制作像Udemy市场这样的 LMS 网站。 目录 Udemy市场是什么&#xff1f; 创建 Udemy 克隆所需的几项强制性技术&#xff1a; 步骤 1) 注册您的域名 步骤 2) 获取虚拟主…

MYSQL篇--sql优化高频面试题

sql优化 1 如何定位及优化SQL语句的性能问题&#xff1f;创建的索引有没有被使用到?或者说怎么才可以知道这条语句运行很慢的原因&#xff1f; 其实对于性能比较低的sql语句定位&#xff0c;最重要的也是最有效的方法其实还是看sql的执行计划&#xff0c;而对于mysql来说 它…

dhcp 时间同步 详细介绍

装服务程序步骤 1.如果有默认配置 请先备份 再进行修改 2.修改完配置文件 请重启服务或重新加载配置文件 否则不生效 注意&#xff1a;有的软件 安装包的名字和 系统里服务程序的名字不一样 htttp httpd openssh-server ssh 高阶级改防火墙 一&#xff0c; dhcp自动分配IP地…

适合PC端的7款最佳时间规划、项目管理软件

分享PC端7类主流的时间管理规划软件&#xff1a;PingCode、Worktile、Todoist、Pomodoro Timer 、Toggl等。 一、时间管理软件的类型 时间管理软件可以根据其功能和应用场景被划分为几种不同的类型。每种类型的软件都旨在帮助用户以不同的方式更有效地管理和分配他们的时间。以…

构建基于RHEL8系列(CentOS8,AlmaLinux8,RockyLinux8等)的支持63个常见模块的PHP8.1.20的RPM包

本文适用&#xff1a;rhel8系列&#xff0c;或同类系统(CentOS8,AlmaLinux8,RockyLinux8等) 文档形成时期&#xff1a;2023年 因系统版本不同&#xff0c;构建部署应略有差异&#xff0c;但本文未做细分&#xff0c;对稍有经验者应不存在明显障碍。 因软件世界之复杂和个人能力…

【Git】查看凭据管理器的账号信息,并删除账号,解决首次认证登录失败后无法重新登录的问题

欢迎来到《小5讲堂》 大家好&#xff0c;我是全栈小5。 这是是《代码管理工具》序列文章&#xff0c;每篇文章将以博主理解的角度展开讲解&#xff0c; 特别是针对知识点的概念进行叙说&#xff0c;大部分文章将会对这些概念进行实际例子验证&#xff0c;以此达到加深对知识点的…

springIoc依赖注入循环依赖三级缓存

springIoc的理解&#xff0c;原理和实现 控制反转&#xff1a; 理论思想&#xff0c;原来的对象是由使用者来进行控制&#xff0c;有了spring之后&#xff0c;可以把整个对象交给spring来帮我们进行管理 依赖注入DI&#xff1a; 依赖注入&#xff0c;把对应的属性的值注入到…

SQL-修改表操作

&#x1f389;欢迎您来到我的MySQL基础复习专栏 ☆* o(≧▽≦)o *☆哈喽~我是小小恶斯法克&#x1f379; ✨博客主页&#xff1a;小小恶斯法克的博客 &#x1f388;该系列文章专栏&#xff1a;重拾MySQL &#x1f379;文章作者技术和水平很有限&#xff0c;如果文中出现错误&am…

运动模型非线性扩展卡尔曼跟踪融合滤波算法(Matlab仿真)

卡尔曼滤波的原理和理论在CSDN已有很多文章&#xff0c;这里不再赘述&#xff0c;仅分享个人的理解和Matlab仿真代码。 1 单目标跟踪 匀速转弯&#xff08;CTRV&#xff09;运动模型下&#xff0c;摄像头输出目标状态camera_state [x, y, theta, v]&#xff0c;雷达输出目标状…

VS中动态库的创建和调用

VS中动态库的创建和调用 库 ​ 库是写好的现有的&#xff0c;成熟的&#xff0c;可以复用的代码。库的存在形式本质上来说库是一种可执行代码的二进制。 ​ 库有两种&#xff1a;静态库&#xff08;.a、.lib&#xff09;和动态库&#xff08;.so、.dll&#xff09;。所谓静态…

Find My游戏手柄|苹果Find My技术与手柄结合,智能防丢,全球定位

游戏手柄是一种常见电子游戏机的部件&#xff0c;通过操纵其按钮等&#xff0c;实现对游戏虚拟角色的控制。随着游戏设备硬件的升级换代&#xff0c;现代游戏手柄又增加了&#xff1a;类比摇杆&#xff08;方向及视角&#xff09;&#xff0c;扳机键以及HOME菜单键等。现在的游…

uniapp 文字超出多少字,显示收起全文按钮效果demo(整理)

收起展开 <template><view class"font30 color000 mL30 mR30"><text :class"showFullText ? : clamp-text">{{ text }}</text><view v-if"showToggleBtn && text.length > 42" click"toggleShowFu…

Elasticsearch安装Windows版

目录 1.&#xff1a;下载安装包&#xff0c;选择指定的版本&#xff0c;这里选择了7.8.0&#xff0c;官网下载地址&#xff1a; ​编辑 2&#xff1a;下载好之后解压&#xff0c;解压之后是这样的&#xff1a; 3&#xff1a;配置环境变量&#xff0c;跟JDK一样&#xff0c;…

如何高效编写测试用例

本话题暂不探讨是否有必要编写详细的测试用例&#xff0c;在确定要交付详细的测试用例这个前提下&#xff0c;分享如何更高效地完成测试用例的编写。 对齐测试用例需求 首先、明确要完成的测试用例文档目标要求&#xff0c;模板、范围、粒度等。 用例文档使用者&#xff1a;…

JDBC初体验(二)——增、删、改、查

本课目标 理解SQL注入的概念 掌握 PreparedStatement 接口的使用 熟练使用JDBC完成数据库的增、删、改、查操作 SQL注入 注入原理&#xff1a;利用现有应用程序&#xff0c;将&#xff08;恶意的&#xff09;SQL命令注入到后台数据库引擎执行能力&#xff0c;它可以通过在…