第十四章 : Spring Boot 整合spring-session,使用redis共享

第十四章 : Spring Boot 整合spring-session,使用redis共享

前沿

本文重点讲述:spring boot工程中使用spring-session机制进行安全认证,并且通过redis存储session,满足集群部署、分布式系统的session共享。

基于SPringBoot 2.3.2.RELEASE

背景

在传统单机web应用中,一般使用tomcat/jetty等web容器时,用户的session都是由容器管理。浏览器使用cookie中记录sessionId,容器根据sessionId判断用户是否存在会话session。这里的限制是,session存储在web容器中,被单台服务器容器管理。

但是网站主键演变,分布式应用和集群是趋势(提高性能)。此时用户的请求可能被负载分发至不同的服务器,此时传统的web容器管理用户会话session的方式即行不通。除非集群或者分布式web应用能够共享session,尽管tomcat等支持这样做。但是这样存在以下两点问题:

1、需要侵入web容器,提高问题的复杂
2、web容器之间共享session,集群机器之间势必要交互耦合

基于这些,必须提供新的可靠的集群分布式/集群session的解决方案,突破traditional-session单机限制(即web容器session方式,下面简称traditional-session),spring-session应用而生。

traditional-session和spring-session的区别

在这里插入图片描述

springboot-session 集成redis示例
  1. 添加依赖:在pom.xml文件中添加Spring Session Redis的依赖。
<dependency>  <groupId>org.springframework.session</groupId>  <artifactId>spring-session-data-redis</artifactId>  
</dependency>
<dependency><groupId>io.lettuce</groupId><artifactId>lettuce-core</artifactId>
</dependency><!-- 对象池,使用redis时必须引入 -->
<dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId>
</dependency>
  1. 配置Redis:在application.yaml文件中添加Redis的配置信息,包括Redis的地址、端口号、密码等。
server:port: 8080servlet:context-path: /# session超时时间 默认30分钟session:timeout: 30m
spring:session:store-type: redisredis:# 会话刷新模式flush-mode: immediate# 用于存储会话的键的命名空间namespace: "spring:session"redis:host: 192.168.92.105port: 6379password: foobared# 连接超时时间(记得添加单位,Duration)timeout: 10000ms# Redis默认情况下有16个分片,这里配置具体使用的分片# database: 0lettuce:pool:# 连接池最大连接数(使用负值表示没有限制) 默认 8max-active: 8# 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1max-wait: -1ms# 连接池中的最大空闲连接 默认 8max-idle: 8# 连接池中的最小空闲连接 默认 0min-idle: 0
  1. 注解开启session功能:并使用@EnableRedisHttpSession注解开启session功能。同时,可以设置session的超时时间。

    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;@SpringBootApplication
    @EnableRedisHttpSession
    public class SpringbootDay09Application {public static void main(String[] args) {SpringApplication.run(SpringbootDay09Application.class, args);}}
    
  2. 创建Controller:在需要使用session的Controller中,注入HttpSession对象,并使用它来存储session数据。

    @RestController  
    public class SessionController {  @Autowired  private HttpSession httpSession;  @GetMapping("/set")  public String set(String name, String value) {  httpSession.setAttribute(name, value);  return "set " + name + "=" + value;  }  @GetMapping("/get")  public String get(String name) {  return (String) httpSession.getAttribute(name);  }  
    }
    
  3. 测试:分别调用各应用接口,查看sessionId是否一致。同时,可以查看Redis缓存信息,缓存中的sessionId与接口返回信息一致。

    在这里插入图片描述

    在这里插入图片描述

spring-session特点与工作原理
特点

spring-session在无需绑定web容器的情况下提供对集群session的支持。并提供对以下情况的透明集成:

  1. HttpSession:容许替换web容器的HttpSession
  2. WebSocket:使用WebSocket通信时,提供Session的活跃
  3. WebSession:容许以应用中立的方式替换webflux的webSession
工作原理
spring-session分为以下核心模块:
  • SessionRepositoryFilter:Servlet规范中Filter的实现,用来切换HttpSession至Spring Session,包装

    HttpServletRequest和HttpServletResponse

  • HttpServerletRequestWrapperHttpServletResponseWrapperHttpSessionWrapper包装器:包装原有的HttpServletRequest、HttpServletResponse和Spring Session,实现切换Session和透明继承HttpSession的关键之所在

  • Session:Spring Session模块

  • SessionRepository:管理Spring Session的模块

  • HttpSessionStrategy:映射HttpRequest和HttpResponse到Session的策略

    在这里插入图片描述

  1. SessionRepositoryFilter

    SessionRepositoryFilter继承OncePerRequestFilter实现Filter

protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {// 设置SessionRepository至Request的属性中request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);// 包装原始HttpServletRequest至SessionRepositoryRequestWrapperSessionRepositoryFilter<S>.SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryFilter.SessionRepositoryRequestWrapper(request, response);// 包装原始HttpServletResponse响应至SessionRepositoryResponseWrapperSessionRepositoryFilter.SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryFilter.SessionRepositoryResponseWrapper(wrappedRequest, response);try {filterChain.doFilter(wrappedRequest, wrappedResponse);} finally {// 提交sessionwrappedRequest.commitSession();}}

2、 SessionRepository

public interface SessionRepository<S extends Session> {S createSession();void save(S var1);S findById(String var1);void deleteById(String var1);
}

创建、保存、获取、删除Session的接口行为。根据Session的不同,分为很多种Session操作仓库。

在这里插入图片描述

当创建一个RedisSession,然后存储在Redis中时,RedisSession的存储细节如下:

spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe
spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe
spring:session:expirations:1439245080000

Redis会为每个RedisSession存储三个k-v。

第一个:k-v用来存储Session的详细信息,包括Session的过期时间间隔、最近的访问时间、attributes等等。这个k的过期时间为Session的最大过期时间 + 5分钟。如果默认的最大过期时间为30分钟,则这个k的过期时间为35分钟
第二个:k-v用来表示Session在Redis中的过期,这个k-v不存储任何有用数据,只是表示Session过期而设置。这个k在Redis中的过期时间即为Session的过期时间间隔
第三个:k-v存储这个Session的id,是一个Set类型的Redis数据结构。这个k中的最后的1439245080000值是一个时间戳,根据这个Session过期时刻滚动至下一分钟而计算得出。

3、 Session

spring-session和tomcat中的Session的实现模式上有很大不同,tomcat中直接对HttpSession接口进行实现,而spring-session中则抽象出单独的Session层接口,让后再使用适配器模式将Session适配层Servlet规范中的HttpSession。spring-sesion中关于session的实现和适配整个UML类图如下:

在这里插入图片描述

MapSession的代码源码片段

public final class MapSession implements Session, Serializable {public static final int DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS = 1800;private String id;private final String originalId;private Map<String, Object> sessionAttrs;private Instant creationTime;private Instant lastAccessedTime;private Duration maxInactiveInterval;private static final long serialVersionUID = 7160779239673823561L;public MapSession() {this(generateId());}public MapSession(String id) {this.sessionAttrs = new HashMap();this.creationTime = Instant.now();this.lastAccessedTime = this.creationTime;this.maxInactiveInterval = Duration.ofSeconds(1800L);this.id = id;this.originalId = id;}public MapSession(Session session) {this.sessionAttrs = new HashMap();this.creationTime = Instant.now();this.lastAccessedTime = this.creationTime;this.maxInactiveInterval = Duration.ofSeconds(1800L);if (session == null) {throw new IllegalArgumentException("session cannot be null");} else {this.id = session.getId();this.originalId = this.id;this.sessionAttrs = new HashMap(session.getAttributeNames().size());Iterator var2 = session.getAttributeNames().iterator();while(var2.hasNext()) {String attrName = (String)var2.next();Object attrValue = session.getAttribute(attrName);if (attrValue != null) {this.sessionAttrs.put(attrName, attrValue);}}this.lastAccessedTime = session.getLastAccessedTime();this.creationTime = session.getCreationTime();this.maxInactiveInterval = session.getMaxInactiveInterval();}}public void setLastAccessedTime(Instant lastAccessedTime) {this.lastAccessedTime = lastAccessedTime;}public Instant getCreationTime() {return this.creationTime;}public String getId() {return this.id;}public String getOriginalId() {return this.originalId;}public String changeSessionId() {String changedId = generateId();this.setId(changedId);return changedId;}public Instant getLastAccessedTime() {return this.lastAccessedTime;}public void setMaxInactiveInterval(Duration interval) {this.maxInactiveInterval = interval;}public Duration getMaxInactiveInterval() {return this.maxInactiveInterval;}public boolean isExpired() {return this.isExpired(Instant.now());}boolean isExpired(Instant now) {if (this.maxInactiveInterval.isNegative()) {return false;} else {return now.minus(this.maxInactiveInterval).compareTo(this.lastAccessedTime) >= 0;}}public <T> T getAttribute(String attributeName) {return this.sessionAttrs.get(attributeName);}public Set<String> getAttributeNames() {return new HashSet(this.sessionAttrs.keySet());}public void setAttribute(String attributeName, Object attributeValue) {if (attributeValue == null) {this.removeAttribute(attributeName);} else {this.sessionAttrs.put(attributeName, attributeValue);}}public void removeAttribute(String attributeName) {this.sessionAttrs.remove(attributeName);}public void setCreationTime(Instant creationTime) {this.creationTime = creationTime;}public void setId(String id) {this.id = id;}public boolean equals(Object obj) {return obj instanceof Session && this.id.equals(((Session)obj).getId());}public int hashCode() {return this.id.hashCode();}private static String generateId() {return UUID.randomUUID().toString();}
}

RedisSession的代码源码片段

final class RedisSession implements Session {private final MapSession cached;private final Map<String, Object> delta = new HashMap();private boolean isNew;private String originalSessionId;RedisSession(MapSession cached, boolean isNew) {this.cached = cached;this.isNew = isNew;this.originalSessionId = cached.getId();if (this.isNew) {this.delta.put("creationTime", cached.getCreationTime().toEpochMilli());this.delta.put("maxInactiveInterval", (int)cached.getMaxInactiveInterval().getSeconds());this.delta.put("lastAccessedTime", cached.getLastAccessedTime().toEpochMilli());}if (this.isNew || RedisSessionRepository.this.saveMode == SaveMode.ALWAYS) {this.getAttributeNames().forEach((attributeName) -> {this.delta.put(RedisSessionRepository.getAttributeKey(attributeName), cached.getAttribute(attributeName));});}}public String getId() {return this.cached.getId();}public String changeSessionId() {return this.cached.changeSessionId();}public <T> T getAttribute(String attributeName) {T attributeValue = this.cached.getAttribute(attributeName);if (attributeValue != null && RedisSessionRepository.this.saveMode.equals(SaveMode.ON_GET_ATTRIBUTE)) {this.delta.put(RedisSessionRepository.getAttributeKey(attributeName), attributeValue);}return attributeValue;}public Set<String> getAttributeNames() {return this.cached.getAttributeNames();}public void setAttribute(String attributeName, Object attributeValue) {this.cached.setAttribute(attributeName, attributeValue);this.delta.put(RedisSessionRepository.getAttributeKey(attributeName), attributeValue);this.flushIfRequired();}public void removeAttribute(String attributeName) {this.setAttribute(attributeName, (Object)null);}public Instant getCreationTime() {return this.cached.getCreationTime();}public void setLastAccessedTime(Instant lastAccessedTime) {this.cached.setLastAccessedTime(lastAccessedTime);this.delta.put("lastAccessedTime", this.getLastAccessedTime().toEpochMilli());this.flushIfRequired();}public Instant getLastAccessedTime() {return this.cached.getLastAccessedTime();}public void setMaxInactiveInterval(Duration interval) {this.cached.setMaxInactiveInterval(interval);this.delta.put("maxInactiveInterval", (int)this.getMaxInactiveInterval().getSeconds());this.flushIfRequired();}public Duration getMaxInactiveInterval() {return this.cached.getMaxInactiveInterval();}public boolean isExpired() {return this.cached.isExpired();}private void flushIfRequired() {if (RedisSessionRepository.this.flushMode == FlushMode.IMMEDIATE) {this.save();}}private boolean hasChangedSessionId() {return !this.getId().equals(this.originalSessionId);}private void save() {this.saveChangeSessionId();this.saveDelta();if (this.isNew) {this.isNew = false;}}private void saveChangeSessionId() {if (this.hasChangedSessionId()) {if (!this.isNew) {String originalSessionIdKey = RedisSessionRepository.this.getSessionKey(this.originalSessionId);String sessionIdKey = RedisSessionRepository.this.getSessionKey(this.getId());RedisSessionRepository.this.sessionRedisOperations.rename(originalSessionIdKey, sessionIdKey);}this.originalSessionId = this.getId();}}private void saveDelta() {if (!this.delta.isEmpty()) {String key = RedisSessionRepository.this.getSessionKey(this.getId());RedisSessionRepository.this.sessionRedisOperations.opsForHash().putAll(key, new HashMap(this.delta));RedisSessionRepository.this.sessionRedisOperations.expireAt(key, Date.from(Instant.ofEpochMilli(this.getLastAccessedTime().toEpochMilli()).plusSeconds(this.getMaxInactiveInterval().getSeconds())));this.delta.clear();}}}
}

在RedisSession中有两个非常重要的成员属性:

cached:实际上是一个MapSession实例,用于做本地缓存,每次在getAttribute时无需从Redis中获取,主要为了improve性能
delta:用于跟踪变化数据,做持久化

4、SessionRepositoryRequestWrapper

对于开发人员获取HttpSession的api

HttpServletRequest request = ...;
HttpSession session = request.getSession(true);

在spring session中request的实际类型SessionRepositoryRequestWrapper。调用SessionRepositoryRequestWrapper的getSession方法会触发创建spring session,而非web容器的HttpSession。

SessionRepositoryRequestWrapper用来包装原始的HttpServletRequest实现HttpSession切换至Spring Session。是透明Spring Session透明集成HttpSession的关键。

SessionRepositoryRequestWrapper继承HttpServletRequestWrapper,在构造方法中将原有的HttpServletRequest通过调用super完成对HttpServletRequestWrapper中持有的HttpServletRequest初始化赋值,然后重写和session相关的方法。这样就保证SessionRepositoryRequestWrapper的其他方法调用都是使用原有的HttpServletRequest的数据,只有session相关的是重写的逻辑。

private final class SessionRepositoryRequestWrapper extends HttpServletRequestWrapper {private final HttpServletResponse response;private S requestedSession;private boolean requestedSessionCached;private String requestedSessionId;private Boolean requestedSessionIdValid;private boolean requestedSessionInvalidated;private SessionRepositoryRequestWrapper(HttpServletRequest request, HttpServletResponse response) {super(request);this.response = response;}private void commitSession() {SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper.HttpSessionWrapper wrappedSession = this.getCurrentSession();if (wrappedSession == null) {if (this.isInvalidateClientSession()) {SessionRepositoryFilter.this.httpSessionIdResolver.expireSession(this, this.response);}} else {S session = wrappedSession.getSession();this.clearRequestedSessionCache();SessionRepositoryFilter.this.sessionRepository.save(session);String sessionId = session.getId();if (!this.isRequestedSessionIdValid() || !sessionId.equals(this.getRequestedSessionId())) {SessionRepositoryFilter.this.httpSessionIdResolver.setSessionId(this, this.response, sessionId);}}}private SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper.HttpSessionWrapper getCurrentSession() {return (SessionRepositoryFilter.SessionRepositoryRequestWrapper.HttpSessionWrapper)this.getAttribute(SessionRepositoryFilter.CURRENT_SESSION_ATTR);}private void setCurrentSession(SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper.HttpSessionWrapper currentSession) {if (currentSession == null) {this.removeAttribute(SessionRepositoryFilter.CURRENT_SESSION_ATTR);} else {this.setAttribute(SessionRepositoryFilter.CURRENT_SESSION_ATTR, currentSession);}}public String changeSessionId() {HttpSession session = this.getSession(false);if (session == null) {throw new IllegalStateException("Cannot change session ID. There is no session associated with this request.");} else {return this.getCurrentSession().getSession().changeSessionId();}}public boolean isRequestedSessionIdValid() {if (this.requestedSessionIdValid == null) {S requestedSession = this.getRequestedSession();if (requestedSession != null) {requestedSession.setLastAccessedTime(Instant.now());}return this.isRequestedSessionIdValid(requestedSession);} else {return this.requestedSessionIdValid;}}private boolean isRequestedSessionIdValid(S session) {if (this.requestedSessionIdValid == null) {this.requestedSessionIdValid = session != null;}return this.requestedSessionIdValid;}private boolean isInvalidateClientSession() {return this.getCurrentSession() == null && this.requestedSessionInvalidated;}public SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper.HttpSessionWrapper getSession(boolean create) {SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper.HttpSessionWrapper currentSession = this.getCurrentSession();if (currentSession != null) {return currentSession;} else {S requestedSession = this.getRequestedSession();if (requestedSession != null) {if (this.getAttribute(SessionRepositoryFilter.INVALID_SESSION_ID_ATTR) == null) {requestedSession.setLastAccessedTime(Instant.now());this.requestedSessionIdValid = true;currentSession = new SessionRepositoryFilter.SessionRepositoryRequestWrapper.HttpSessionWrapper(requestedSession, this.getServletContext());currentSession.markNotNew();this.setCurrentSession(currentSession);return currentSession;}} else {if (SessionRepositoryFilter.SESSION_LOGGER.isDebugEnabled()) {SessionRepositoryFilter.SESSION_LOGGER.debug("No session found by id: Caching result for getSession(false) for this HttpServletRequest.");}this.setAttribute(SessionRepositoryFilter.INVALID_SESSION_ID_ATTR, "true");}if (!create) {return null;} else {if (SessionRepositoryFilter.SESSION_LOGGER.isDebugEnabled()) {SessionRepositoryFilter.SESSION_LOGGER.debug("A new session was created. To help you troubleshoot where the session was created we provided a StackTrace (this is not an error). You can prevent this from appearing by disabling DEBUG logging for " + SessionRepositoryFilter.SESSION_LOGGER_NAME, new RuntimeException("For debugging purposes only (not an error)"));}S session = SessionRepositoryFilter.this.sessionRepository.createSession();session.setLastAccessedTime(Instant.now());currentSession = new SessionRepositoryFilter.SessionRepositoryRequestWrapper.HttpSessionWrapper(session, this.getServletContext());this.setCurrentSession(currentSession);return currentSession;}}}public SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper.HttpSessionWrapper getSession() {return this.getSession(true);}public String getRequestedSessionId() {if (this.requestedSessionId == null) {this.getRequestedSession();}return this.requestedSessionId;}public RequestDispatcher getRequestDispatcher(String path) {RequestDispatcher requestDispatcher = super.getRequestDispatcher(path);return new SessionRepositoryFilter.SessionRepositoryRequestWrapper.SessionCommittingRequestDispatcher(requestDispatcher);}private S getRequestedSession() {if (!this.requestedSessionCached) {List<String> sessionIds = SessionRepositoryFilter.this.httpSessionIdResolver.resolveSessionIds(this);Iterator var2 = sessionIds.iterator();while(var2.hasNext()) {String sessionId = (String)var2.next();if (this.requestedSessionId == null) {this.requestedSessionId = sessionId;}S session = SessionRepositoryFilter.this.sessionRepository.findById(sessionId);if (session != null) {this.requestedSession = session;this.requestedSessionId = sessionId;break;}}this.requestedSessionCached = true;}return this.requestedSession;}private void clearRequestedSessionCache() {this.requestedSessionCached = false;this.requestedSession = null;this.requestedSessionId = null;}

5、 SessionRepositoryResponseWrapper

private final class SessionRepositoryResponseWrapper extends OnCommittedResponseWrapper {private final SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper request;SessionRepositoryResponseWrapper(SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper request, HttpServletResponse response) {super(response);if (request == null) {throw new IllegalArgumentException("request cannot be null");} else {this.request = request;}}protected void onResponseCommitted() {this.request.commitSession();}}
5、 SessionRepositoryResponseWrapper ```java
private final class SessionRepositoryResponseWrapper extends OnCommittedResponseWrapper {private final SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper request;SessionRepositoryResponseWrapper(SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper request, HttpServletResponse response) {super(response);if (request == null) {throw new IllegalArgumentException("request cannot be null");} else {this.request = request;}}protected void onResponseCommitted() {this.request.commitSession();}}

从注释上可以看出包装响应时为了:确保如果响应被提交session能够被保存。
在这里插入图片描述

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

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

相关文章

[linux运维] 利用zabbix监控linux高危命令并发送告警(基于Zabbix 6)

之前写过一篇是基于zabbix 5.4的实现文章&#xff0c;但是不太详细&#xff0c;最近已经有两个小伙伴在zabbix 6上操作&#xff0c;发现触发器没有str函数&#xff0c;所以更新一下本文&#xff0c;基于zabbix 6 0x01 来看看效果 高危指令出发问题告警&#xff1a; 发出邮件告…

学好操作系统需要的前置知识

1. 态度&#xff1a;不要等一切都准备好了再前行 如果把一切你可能会说&#xff0c;没有这些基础知识&#xff0c;我每看一篇文章&#xff0c;知识就铺天盖地席卷过来&#xff0c;仿佛每一个知识点都准确地打在了自己的盲点上&#xff0c;这该怎么办呢&#xff1f; 我非常能理…

AI助力智慧农业,基于YOLOv8全系列模型【n/s/m/l/x】开发构建不同参数量级的识别系统

智慧农业随着数字化信息化浪潮的演变有了新的定义&#xff0c;在前面的系列博文中&#xff0c;我们从一些现实世界里面的所见所想所感进行了很多对应的实践&#xff0c;感兴趣的话可以自行移步阅读即可&#xff1a; 《自建数据集&#xff0c;基于YOLOv7开发构建农田场景下杂草…

05 JQuery基础入门

文章目录 一、jQuery介绍1. 简介2. 版本介绍3. 相关网站4. HTML引入方式 二、基础语法1. 顶级对象$2. 与DOM对象转化3. 选择器4. 事件5. 动画6. 修改样式7. 修改属性 一、jQuery介绍 1. 简介 jQuery是JavaScript编程语言底层库&#xff0c;它是一个快速&#xff0c;简洁的Jav…

ERPNext SQL 注入漏洞复现

0x01 产品简介 ERPNext 是一套开源的企业资源计划系统。 0x02 漏洞概述 ERPNext 系统frappe.model.db_query.get_list 文件 filters 参数存在 SQL 注入漏洞,攻击者除了可以利用 SQL 注入漏洞获取数据库中的信息(例如,管理员后台密码、站点的用户个人信息)之外,甚至在高权…

基于springboot实现的仿天猫商城项目

一、系统架构 前端&#xff1a;jsp | js | css | jquery 后端&#xff1a;springboot | mybatis-plus 环境&#xff1a;jdk1.7 | mysql | maven 二、代码及数据库 三、功能介绍 01. web端-首页 02. web端-商品查询 03. web端-商品详情 04. web端-购物车 05. web端-订单…

集合的几个遍历方法

1. 集合的遍历 1.0 创建集合代码 List<String> strList new ArrayList<>(); strList.add("huawei"); strList.add("xiaomi"); strList.add("tencent"); strList.add("google"); strList.add("baidu");1.1 fo…

复杂gRPC之go调用go

1. 复杂的gRPC调用 我们使用了一个较为复杂的proto文件&#xff0c;这个文件的功能主要是用来定位的&#xff0c;详细内容可以看代码中的注解 syntax "proto3"; //指定生成的所属的package&#xff0c;方便调用 option go_package "./"; package route…

Redis和MySQL双写一致性实用解析

1、背景 先阐明一下Mysql和Redis的关系&#xff1a;Mysql是数据库&#xff0c;用来持久化数据&#xff0c;一定程度上保证数据的可靠性&#xff1b;Redis是用来当缓存&#xff0c;用来提升数据访问的性能。 关于如何保证Mysql和Redis中的数据一致&#xff08;即缓存一致性问题…

labelme等标注工具/数据增强工具输出JSON文件格式检查脚本

标注的文件太多了&#xff0c;还有用数据增强工具生成了一票的新数据。在转换或使用训练时候会报错&#xff0c;错误原因是json中语法有问题&#xff0c;这样会中断程序运行&#xff0c;调试造成很大困扰。 检查确实最后有问题&#xff0c;多写了一次 写一个脚本&#xff0c;用…

Python-滑雪大冒险【附源码】

滑雪大冒险 《滑雪大冒险》是一款充满趣味性和挑战性的休闲竞技游戏&#xff0c;在游戏中&#xff0c;玩家将扮演一位勇敢的滑雪者&#xff0c;在雪山上展示他们的滑雪技巧&#xff0c;游戏采用2D图形界面&#xff0c;以第三人称视角呈现 运行效果&#xff1a;用方向键及方向键…

flask 数据库迁移可能出现的六大问题,生成requirements文件夹方式,flask项目复写,

今日任务 项目分级显示 — app — — admin 代表 — — auth 代表用户的点赞 评论 登录等等 — — blog 代表blog的网页 首先单独把auth运行出来 第一步 1. 生成requirements文件夹 2.在一个新的虚拟环境里面完成requirements依赖下载 3.完成项目的复写 1. 生成requ…

算术运算(这么简单?进来坐坐?)

先热热身 算术运算&#xff0c;也称为四则运算&#xff0c;包括加法、减法、乘法和除法。此外&#xff0c;算术运算还包括乘方和开方。 在算术中&#xff0c;加减被视为一级运算&#xff0c;乘除被视为二级运算&#xff0c;乘方和开方被视为三级运算。在一道算式中&#xff0c;…

网站导航栏下滑隐藏,上滑显示,效果杠杆,兼容性强

前言 导航栏是网站必不可少的一部分&#xff0c;那么&#xff0c;导航栏应该怎么样子实现&#xff0c;可以高效自定义兼容开发呢&#xff1f;当然&#xff0c;不仅要实现&#xff0c;而且还要实现导航栏顶部固定位置&#xff0c;下拉隐藏&#xff0c;稍微往上滑动就会出现&…

Python中的并发编程(2)线程的实现

Python中线程的实现 1. 线程 在Python中&#xff0c;threading 库提供了线程的接口。我们通过threading 中提供的接口创建、启动、同步线程。 例1. 使用线程旋转指针 想象一个场景&#xff1a;程序执行了一个耗时较长的操作&#xff0c;如复制一个大文件&#xff0c;我们希…

2022年第十一届数学建模国际赛小美赛D题野生动物贸易是否应长期禁止解题全过程文档及程序

2022年第十一届数学建模国际赛小美赛 D题 野生动物贸易是否应长期禁止 原题再现&#xff1a; 野生动物市场被怀疑是此次疫情和2002年SARS疫情的源头&#xff0c;食用野生肉类被认为是非洲埃博拉病毒的一个来源。在冠状病毒爆发后&#xff0c;中国最高立法机构永久性地加强了野…

【git教程】

目录 git与SVN的区别&#xff1a;集中式与分布式的区别Windows上安装Git创建版本库/仓库&#xff08;repository&#xff09;将文件添加到repository报错处理 查看仓库的状态版本回退工作区和暂存区管理和修改撤销修改删除文件远程仓库添加远程仓库警告解除本地和远程的绑定关系…

一文3000字从0到1用Python进行gRPC接口测试!

gRPC 是一个高性能、通用的开源RPC框架&#xff0c;其由 Google 主要面向移动应用开发并基于HTTP/2 协议标准而设计&#xff0c;基于 ProtoBuf(Protocol Buffers) 序列化协议开发&#xff0c;且支持众多开发语言。 自gRPC推出以来&#xff0c;已经广泛应用于各种服务之中。在测…

AI助力智慧农业,基于SSD模型开发构建田间作物场景下庄稼作物、杂草检测识别系统

智慧农业随着数字化信息化浪潮的演变有了新的定义&#xff0c;在前面的系列博文中&#xff0c;我们从一些现实世界里面的所见所想所感进行了很多对应的实践&#xff0c;感兴趣的话可以自行移步阅读即可&#xff1a;《自建数据集&#xff0c;基于YOLOv7开发构建农田场景下杂草检…

【C语言快速学习基础篇】之二控制语句、循环语句、隐式转换

文章目录 一、控制语句1.1、for循环1.2、while循环1.3、注意&#xff1a;for循环和while循环使用上面等同1.4、do while循环1.4.1while条件成立时1.4.2、while条件不成立时 C语言介绍 C语言是一门面向过程的计算机编程语言&#xff0c;与C、C#、Java等面向对象编程语言有所不同…