分布式环境下Spring Session Redis底层原理

news2024/10/6 6:45:42

1 自动装配

 public class SessionAutoConfiguration {
     
   // SessionRepositoryFilterConfiguration用来配置核心的过滤器
   // 3 核心过滤器
   @Configuration(proxyBeanMethods = false)
   @ConditionalOnWebApplication(type = Type.SERVLET)
   @Import({ ServletSessionRepositoryValidator.class, SessionRepositoryFilterConfiguration.class })
   static class ServletSessionConfiguration {
 
     @Configuration(proxyBeanMethods = false)
     @ConditionalOnMissingBean(SessionRepository.class)
     @Import({ ServletSessionRepositoryImplementationValidator.class,
         ServletSessionConfigurationImportSelector.class })
     static class ServletSessionRepositoryConfiguration {
     }
 
   }
     // 该类主要作用就是用来更加当前环境下的所有类型的*SessionConfiguration
     // 如:RedisSessionConfiguration,JdbcSessionConfiguration等。
     // 2 核心Session配置对象
     static class ServletSessionConfigurationImportSelector extends SessionConfigurationImportSelector {
     @Override
     public String[] selectImports(AnnotationMetadata importingClassMetadata) {
       return super.selectImports(WebApplicationType.SERVLET);
     }
   }
 }

2 核心Session配置对象

在上每一步中会获取容器中所有注册的*SessionConfiguration。

  1. RedisSessionConfiguration,RedisReactiveSessionConfiguration
  2. MongoSessionConfiguration,MongoReactiveSessionConfiguration
  3. JdbcSessionConfiguration
  4. HazelcastSessionConfiguration
  5. NoOpSessionConfiguration,NoOpReactiveSessionConfiguration

这些类都是在如下类中注册

 final class SessionStoreMappings {
 
   private static final Map<StoreType, Configurations> MAPPINGS;
 
   static {
     Map<StoreType, Configurations> mappings = new EnumMap<>(StoreType.class);
     mappings.put(StoreType.REDIS,
         new Configurations(RedisSessionConfiguration.class, RedisReactiveSessionConfiguration.class));
     mappings.put(StoreType.MONGODB,
         new Configurations(MongoSessionConfiguration.class, MongoReactiveSessionConfiguration.class));
     mappings.put(StoreType.JDBC, new Configurations(JdbcSessionConfiguration.class, null));
     mappings.put(StoreType.HAZELCAST, new Configurations(HazelcastSessionConfiguration.class, null));
     mappings.put(StoreType.NONE,
         new Configurations(NoOpSessionConfiguration.class, NoOpReactiveSessionConfiguration.class));
     MAPPINGS = Collections.unmodifiableMap(mappings);
   }
 }

2.1 注册Session配置类

上面列出了系统中所有的*SessionConfiguration配置类,那具体该注册哪一个?

回到上面的
ServletSessionConfigurationImportSelector中

进入
ServletSessionConfigurationImportSelector#selectImports方法:

 abstract static class SessionConfigurationImportSelector implements ImportSelector {
     protected final String[] selectImports(WebApplicationType webApplicationType) {
         // 这里就是迭代上面登记的所有*SessionConfiguration类
         return Arrays.stream(StoreType.values())
             .map((type) -> SessionStoreMappings.getConfigurationClass(webApplicationType, type))
             .toArray(String[]::new);
     }
 }

获取到所有的配置类后,如何进行选择该注册哪一个配置类?这里我们打开*SessionConfiguration

 @Configuration(proxyBeanMethods = false)
 @ConditionalOnClass({ RedisTemplate.class, RedisIndexedSessionRepository.class })
 @ConditionalOnMissingBean(SessionRepository.class)
 @ConditionalOnBean(RedisConnectionFactory.class)
 @Conditional(ServletSessionCondition.class)
 @EnableConfigurationProperties(RedisSessionProperties.class)
 class RedisSessionConfiguration {
     @Configuration(proxyBeanMethods = false)
   public static class SpringBootRedisHttpSessionConfiguration extends RedisHttpSessionConfiguration {
     }
 }
 @Configuration(proxyBeanMethods = false)
 @ConditionalOnClass({ JdbcTemplate.class, JdbcIndexedSessionRepository.class })
 @ConditionalOnMissingBean(SessionRepository.class)
 @ConditionalOnBean(DataSource.class)
 @Conditional(ServletSessionCondition.class)
 @EnableConfigurationProperties(JdbcSessionProperties.class)
 class JdbcSessionConfiguration {
     @Configuration(proxyBeanMethods = false)
   static class SpringBootJdbcHttpSessionConfiguration extends JdbcHttpSessionConfiguration {
     }
 }
 @Configuration(proxyBeanMethods = false)
 @ConditionalOnClass({ MongoOperations.class, MongoIndexedSessionRepository.class })
 @ConditionalOnMissingBean(SessionRepository.class)
 @ConditionalOnBean(MongoOperations.class)
 @Conditional(ServletSessionCondition.class)
 @EnableConfigurationProperties(MongoSessionProperties.class)
 class MongoSessionConfiguration {
     @Configuration
     public static class SpringBootMongoHttpSessionConfiguration extends MongoHttpSessionConfiguration {
     }
 }
 @Conditional(ServletSessionCondition.class)
 @ConditionalOnMissingBean(SessionRepository.class)
 class HazelcastSessionConfiguration {
 }
 @Conditional(ServletSessionCondition.class)
 @ConditionalOnMissingBean(SessionRepository.class)
 class NoOpSessionConfiguration {
 }

这些类每一种存储类型它都有相应的注册条件,只有满足条件的才能被注册。

注意:

这些类是通过ImportSelector导入进行注册的,这时候就需要注意了,如果一个类是通过@Import导入的,那么只有导入的这个类能被注册,该类的内部配置类才能被注册,反之,被导入的不能被注册,那么这个类的内部配置类也不会被注册。如下RedisSessionConfiguration,如果这个类不能被注册,那么内部类
SpringBootRedisHttpSessionConfiguration也不能被注册。

 class RedisSessionConfiguration {
     @Configuration(proxyBeanMethods = false)
   public static class SpringBootRedisHttpSessionConfiguration extends RedisHttpSessionConfiguration {
     @Autowired
     public void customize(SessionProperties sessionProperties, RedisSessionProperties redisSessionProperties) {
       Duration timeout = sessionProperties.getTimeout();
       if (timeout != null) {
         setMaxInactiveIntervalInSeconds((int) timeout.getSeconds());
       }
       setRedisNamespace(redisSessionProperties.getNamespace());
       setFlushMode(redisSessionProperties.getFlushMode());
       setSaveMode(redisSessionProperties.getSaveMode());
       setCleanupCron(redisSessionProperties.getCleanupCron());
     }
   }
 }

如果一个配置类本身在容器启动的时候就能被容器扫描到,那么如果该类即便不能被注册,但是他的内部配置类还是可以被注册的。如下情况:

 @Configuration
 @ConditionalOnProperty(prefix = "s", name = "n", havingValue = "1", matchIfMissing = false)
 public class InnerConfiguration {
   
   public InnerConfiguration() {
     System.out.println("===============") ;
   }
   
   @Configuration
   static class Inner {
     public Inner() {
       System.out.println("--------------") ;
     }
   }
   
 }

如果上面的类内被容器启动的时候扫描到,但是这个类本身没有满足条件不能被注册,但是它的内部配置类Inner还是会被容器扫描到进行注册的。因为容器启动的时候会扫描启动类所在的包及其子包下的所有*.class文件,Inner这个内部类也是一个class文件。

再看ServletSessionCondition条件注册类

 class ServletSessionCondition extends AbstractSessionCondition {
 
   ServletSessionCondition() {
     super(WebApplicationType.SERVLET);
   }
 
 }
 abstract class AbstractSessionCondition extends SpringBootCondition {
   private final WebApplicationType webApplicationType;
   protected AbstractSessionCondition(WebApplicationType webApplicationType) {
     this.webApplicationType = webApplicationType;
   }
   @Override
   public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
     ConditionMessage.Builder message = ConditionMessage.forCondition("Session Condition");
     Environment environment = context.getEnvironment();
     StoreType required = SessionStoreMappings.getType(this.webApplicationType,
         ((AnnotationMetadata) metadata).getClassName());
     if (!environment.containsProperty("spring.session.store-type")) {
       return ConditionOutcome.match(message.didNotFind("property", "properties")
           .items(ConditionMessage.Style.QUOTE, "spring.session.store-type"));
     }
     try {
       Binder binder = Binder.get(environment);
             // 将spring.session.store-type配置属性绑定到StoreType枚举对象上
       return binder.bind("spring.session.store-type", StoreType.class)
                   // 判断配置的类型是否与当前处理的类上的相同。
           .map((t) -> new ConditionOutcome(t == required,
               message.found("spring.session.store-type property").items(t)))
           .orElse(ConditionOutcome.noMatch(message.didNotFind("spring.session.store-type property").atAll()));
     }
   }
 
 }

2.2 注册Session存储对象

这里以Redis为例,上面的
SpringBootRedisHttpSessionConfiguration继承
RedisHttpSessionConfiguration类进入

 @Configuration(proxyBeanMethods = false)
 public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration
     implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware {
     // 注册一个SessionRepository类型的Session存储对象
     @Bean
   public RedisIndexedSessionRepository sessionRepository() {
         // ...
     }
 }

到这里最为关键的一个SessionRepository对象就创建注册了。


RedisIndexedSessionRepository类继承自SessionRepository接口。

3 核心过滤器

3.1 过滤器注册

 class SessionRepositoryFilterConfiguration {
     // 这里的SessionRepositoryFilter是核心的处理Session的过滤器
     // 而关于该种过滤器的注册方式可参考SpringSecurity.md文档
   @Bean
   FilterRegistrationBean<SessionRepositoryFilter<?>> sessionRepositoryFilterRegistration(
       SessionProperties sessionProperties, SessionRepositoryFilter<?> filter) {
     FilterRegistrationBean<SessionRepositoryFilter<?>> registration = new FilterRegistrationBean<>(filter);
     registration.setDispatcherTypes(getDispatcherTypes(sessionProperties));
     registration.setOrder(sessionProperties.getServlet().getFilterOrder());
     return registration;
   }
 }

在2.2中
RedisHttpSessionConfiguration继承自
SpringHttpSessionConfiguration进入该类

 @Configuration(proxyBeanMethods = false)
 public class SpringHttpSessionConfiguration implements ApplicationContextAware {
     // 注入了在上一步中创建的核心Session存储对象RedisIndexedSessionRepository
     // 该过滤器对象会被注册到Servlet容器中
     @Bean
   public <S extends Session> SessionRepositoryFilter<? extends Session> springSessionRepositoryFilter(
       SessionRepository<S> sessionRepository) {
     SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter<>(sessionRepository);
     sessionRepositoryFilter.setHttpSessionIdResolver(this.httpSessionIdResolver);
     return sessionRepositoryFilter;
   }
 }

3.2 过滤器核心方法

接下来查看该过滤器的一些核心方法

 @Order(SessionRepositoryFilter.DEFAULT_ORDER)
 public class SessionRepositoryFilter<S extends Session> extends OncePerRequestFilter {
     @Override
   protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
       throws ServletException, IOException {
     request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
     // 核心就是这里,分别自定义了Request,Response对象进行了重新包装
     SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(request, response);
     SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(wrappedRequest,
         response);
     try {
             // 将自定义的Request,Response向下传递,这在使用了Spring Security就非常方便了。
       filterChain.doFilter(wrappedRequest, wrappedResponse);
     } finally {
             // 这就就是触发讲所有向Session中存入的对象保存到对应的实现中(如:Redis或JDBC)
       wrappedRequest.commitSession();
     }
   }
 }

接着查看
SessionRepositoryRequestWrapper包装类中重写的几个核心方法

 private final class SessionRepositoryRequestWrapper extends HttpServletRequestWrapper {
     // 提交Session中的数据保存到具体的实现中,如(Redis,JDBC等)
     private void commitSession() {
         HttpSessionWrapper wrappedSession = getCurrentSession();
         if (wrappedSession == null) {
             if (isInvalidateClientSession()) {
                 SessionRepositoryFilter.this.httpSessionIdResolver.expireSession(this, this.response);
             }
         } else {
             S session = wrappedSession.getSession();
             clearRequestedSessionCache();
             SessionRepositoryFilter.this.sessionRepository.save(session);
             String sessionId = session.getId();
             if (!isRequestedSessionIdValid() || !sessionId.equals(getRequestedSessionId())) {
                 SessionRepositoryFilter.this.httpSessionIdResolver.setSessionId(this, this.response, sessionId);
             }
         }
   }
     // 当通过HttpServletRequest获取HttpSession对象的时候就是调用的该方法了。
     @Override
     public HttpSessionWrapper getSession(boolean create) {
         HttpSessionWrapper currentSession = getCurrentSession();
         if (currentSession != null) {
             return currentSession;
         }
         S requestedSession = getRequestedSession();
         if (requestedSession != null) {
             if (getAttribute(INVALID_SESSION_ID_ATTR) == null) {
                 requestedSession.setLastAccessedTime(Instant.now());
                 this.requestedSessionIdValid = true;
                 currentSession = new HttpSessionWrapper(requestedSession, getServletContext());
                 currentSession.markNotNew();
                 setCurrentSession(currentSession);
                 return currentSession;
             }
         } else {
             setAttribute(INVALID_SESSION_ID_ATTR, "true");
         }
         if (!create) {
             return null;
         }
         // session = MapSession该对象内部维护了一个Map集合
         S session = SessionRepositoryFilter.this.sessionRepository.createSession();
         session.setLastAccessedTime(Instant.now());
         // 这又是自定义的Session对象
         currentSession = new HttpSessionWrapper(session, getServletContext());
         setCurrentSession(currentSession);
         return currentSession;
     }
 
     @Override
     public HttpSessionWrapper getSession() {
         return getSession(true);
     }
     // 实际操作的Session对象就是该实现
     private final class HttpSessionWrapper extends HttpSessionAdapter<S> {
         HttpSessionWrapper(S session, ServletContext servletContext) {
             super(session, servletContext);
         }
         @Override
         public void invalidate() {
             super.invalidate();
             SessionRepositoryRequestWrapper.this.requestedSessionInvalidated = true;
             setCurrentSession(null);
             clearRequestedSessionCache();
             SessionRepositoryFilter.this.sessionRepository.deleteById(getId());
         }
 
     }
 }

像Session中操作数据核心方法是setAttribute,getAttribute

HttpSessionWrapper继承HttpSessionWrapper

 class HttpSessionAdapter<S extends Session> implements HttpSession {
     // MapSession 内部维护了一个Map集合,专门用来存数据的
     private S session;
     public Object getAttribute(String name) {
     return this.session.getAttribute(name);
   }
     public void setAttribute(String name, Object value) {
     checkState();
         // 调用MapSession对象方法,获取内部Map中的值信息
     Object oldValue = this.session.getAttribute(name);
         // 调用MapSession对象方法,将键值存入到内部维护的Map中
     this.session.setAttribute(name, value);
     if (value != oldValue) {
       if (oldValue instanceof HttpSessionBindingListener) {
         try {
           ((HttpSessionBindingListener) oldValue)
               .valueUnbound(new HttpSessionBindingEvent(this, name, oldValue));
         }
       }
       if (value instanceof HttpSessionBindingListener) {
         try {
           ((HttpSessionBindingListener) value).valueBound(new HttpSessionBindingEvent(this, name, value));
         }
       }
     }
   }
 }

该过滤器的作用及工作流程总结如下:

  1. 使用自定义的Request,Response对象将这2个对象通过FilterChain#doFilter方法向后传递,供其它的过滤器使用。
  2. 其它过滤器在使用Session过程中都是使用的上一步中传下来的自定义Request对象SessionRepositoryRequestWrapper
  3. 目标对象执行完后返回时会继续执行FilterChain#doFilter剩下的代码,也就是上面的SessionRepositoryRequestWrapper#commitSession方法,该方法的多用就是提交在后续的Filter或者目标对象(如:Controller)中对Session对象的操作,将这些信息提交多相应的存储对象上,如:Redis或者JDBC等中。

3.3 Session数据存储

这里我们查看关于
SessionRepositoryRequestWrapper#commitSession方法的执行。

根据上面还是以Redis实现为例,Session的存储对象是
RedisIndexedSessionRepository

 private final class SessionRepositoryRequestWrapper extends HttpServletRequestWrapper {
     private void commitSession() {
       HttpSessionWrapper wrappedSession = getCurrentSession();
       if (wrappedSession == null) {
         if (isInvalidateClientSession()) {
           SessionRepositoryFilter.this.httpSessionIdResolver.expireSession(this, this.response);
         }
       } else {
         S session = wrappedSession.getSession();
         clearRequestedSessionCache();
                 // 保存session里的信息
         SessionRepositoryFilter.this.sessionRepository.save(session);
       }
     }
 }


RedisIndexedSessionRepository对象

 public class RedisIndexedSessionRepository
     implements FindByIndexNameSessionRepository<RedisIndexedSessionRepository.RedisSession>, MessageListener {
     public void save(RedisSession session) {
     session.save();
     if (session.isNew) {
       String sessionCreatedKey = getSessionCreatedChannel(session.getId());
       this.sessionRedisOperations.convertAndSend(sessionCreatedKey, session.delta);
       session.isNew = false;
     }
   }
 }

RedisSession对象

 final class RedisSession implements Session {
     // 该Map中存了所有的Session信息
     private Map<String, Object> delta = new HashMap<>();
     private void save() {
         saveChangeSessionId();
         // 这里是核心
         saveDelta();
     }
     private void saveDelta() {
         if (this.delta.isEmpty()) {
             return;
         }
         String sessionId = getId();
         // 将所有的数据保存到Redis中。
         getSessionBoundHashOperations(sessionId).putAll(this.delta);
         String principalSessionKey = getSessionAttrNameKey(
             FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME);
         String securityPrincipalSessionKey = getSessionAttrNameKey(SPRING_SECURITY_CONTEXT);
         if (this.delta.containsKey(principalSessionKey) || this.delta.containsKey(securityPrincipalSessionKey)) {
             if (this.originalPrincipalName != null) {
                 String originalPrincipalRedisKey = getPrincipalKey(this.originalPrincipalName);
                 RedisIndexedSessionRepository.this.sessionRedisOperations.boundSetOps(originalPrincipalRedisKey)
                     .remove(sessionId);
             }
             Map<String, String> indexes = RedisIndexedSessionRepository.this.indexResolver.resolveIndexesFor(this);
             String principal = indexes.get(PRINCIPAL_NAME_INDEX_NAME);
             this.originalPrincipalName = principal;
             if (principal != null) {
                 String principalRedisKey = getPrincipalKey(principal);
                 RedisIndexedSessionRepository.this.sessionRedisOperations.boundSetOps(principalRedisKey)
                     .add(sessionId);
             }
         }
         // 将数据存储完成后将delta集合清空(这里可以避免重复提交数据)
         this.delta = new HashMap<>(this.delta.size());
         // 下面就是更新key的过期时间
         Long originalExpiration = (this.originalLastAccessTime != null)
             ? this.originalLastAccessTime.plus(getMaxInactiveInterval()).toEpochMilli() : null;
         RedisIndexedSessionRepository.this.expirationPolicy.onExpirationUpdated(originalExpiration, this);
     }
 }

完毕!!!

图片

 

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

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

相关文章

什么是代码签名证书?

使用代码签名证书&#xff0c;您可以保证签名者的身份和软件的完整性&#xff0c;这可以防止在下载和安装软件时出现警告。 代码签名证书是软件开发人员用来签署其软件、应用程序和驱动程序代码的数字证书。它使用公私密钥基础设施(PKI)将实体绑定到公钥和私钥。 申请代码签名…

SuperKnob超级旋钮

SuperKnob超级旋钮 2022年12月5日&#xff0c;今天总结下几个月前搞得小项目&#xff0c;好久没有写文总结了&#xff0c;有多少人不再写博客了呢&#xff1f;转眼疫情已三年&#xff0c;生活节奏太快了&#xff0c;认识的很多大佬他们的博客也不再更新了&#xff0c;都在努力…

Vue3 组件,一点也不难

Vue3 组件&#xff0c;一点也不难1.简介2.一个简单的 Vue 组件的实例3.局部组件4.Prop5.动态 Prop1.简介 组件是 Vue.js 最强大的功能之一。组件可以扩展 HTML 元素&#xff0c;封装可重用的代码。 组件系统让我们可以用独立可复用的小组件来构建大型应用&#xff0c;几乎任意…

jmeter压测mysql(保姆级教程)

准备工作&#xff08;下载mysql-connector&#xff09;&#xff1a; 打开mysql官网&#xff1a;MySQL 找到mysql connectors&#xff0c;选择操作系统独立版本&#xff0c;将下载jar包导入到jmeter的lib目录下。 参考文档&#xff1a;https://www.jb51.net/article/190860.h…

安装包UI美化之路-nsNiuniuSkin多语言配置

今天给大家带来的多语言配置功能&#xff0c;算是nsNiuniuSkin的一次质的提升&#xff0c;希望对大家有所帮助。 nsNiuniuSkin通过自研多语言配置引擎&#xff0c;让安装包的多语言配置不再复杂&#xff0c;直接设置语言翻译配置文件&#xff0c;即可实现安装包整体的多语言切…

一文读懂机智云物联网APP开发

本教程可用于C2 DevKit开发板套件全功能展示&#xff0c;用于对shineblink 的C2 DevKit开发套件全面学习使用&#xff0c;shineblink&及机智云技术交流群&#xff08;234976524&#xff09;。开发板相关的进阶教程及DIY应用将在后续持续更新。教程主要包含以下内容&#xf…

客户案例 | 举重若轻,低代码培育核心业务能力工坊

关键发现 用户痛点&#xff1a;传统套件式项目管理软件在中小企业或业务流程相对简单的企业中适配度低&#xff0c;不够灵活&#xff0c;性价比低&#xff0c;需要一套针对不同业务流程可以灵活配置的、轻量化的项目管理系统。 解决方案&#xff1a;基于西门子低代码开发平台…

六、Git远程仓库操作——创建远程库、推送拉取和克隆远程库等操作

1. 远程仓库介绍 前面的关于git的操作&#xff0c;都是基于本地仓库进行操作的。 但是如果我们想通过 Git 分享自己的代码或者与其他开发人员合作&#xff0c;这时我们就需要将数据放到一台其他开发人员能够连接的服务器上&#xff0c;这台服务器就是代码托管中心&#xff0c…

webpack热更新原理解析

热更新原理 1. webpack-dev-server启动本地服务 这里首先会启动webpack并生成compiler实例&#xff08;compiler实例通过各种事件钩子可以实现监听编译无效、编译结束等功能&#xff09;&#xff1b; 然后会通过express启动一个本地服务&#xff0c;用于服务浏览器对打包资源…

前端中的身份认证

一.Cookie 1.1.HTTP协议的无状态性 HTTP协议的无状态&#xff1a; 客户端的每次HTTP请求都是独立的&#xff0c;之间没有直接关系 服务器不会主动保留每次HTTP请求的状态 1.2.任何突破HTTP无状态的限制 发会员卡 注意&#xff1a; ​ 现实中,会员卡身份认证方式&#xff0c;在…

03 - 调试环境的搭建(Bochs)

---- 整理自狄泰软件唐佐林老师课程 1. Bochs&#xff08;另一款优秀的虚拟机软件&#xff09; 专业模拟x86架构的虚拟机 开源且高度可移植&#xff0c;由C编写完成 支持操作系统开发过程中的断点调试 通过简单配置就能运行绝大多数主流的操作系统 2. Bochs的安装与配置 下载…

[附源码]计算机毕业设计社区人员信息管理系统设计与实现Springboot程序

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

HRB系列直流隔离可调电源模块:用20K电位器和200K电位器区别

今天俞霖科技小编简谈如何合理地选用DC-DC模块电源&#xff0c;本文将从DC-DC模块电源开发设计的角度来简谈以上问题&#xff0c;以供广大技术设计人员参考。 DC-DC模块电源的众多优点是大家众所周知的&#xff0c;DC-DC模块电源以其体积小巧、性能卓异、使用方便的显著特点&a…

ArcGIS API For JavaScript(8)之使用动态图层dynamicLayers实现多图层合并截图

场景还原&#xff1a; 定位某个矢量图斑范围面&#xff0c;过滤展示该图斑&#xff0c;以图斑为中心&#xff0c;截图图斑周边并附带影像底图的截图。 在前端要实现地图截图&#xff0c;首先想到的是使用arcgis rest api中的export接口&#xff0c;这是没问题的&#xff0c;exp…

工业物联网关-modbus数据采集程序(1-程序设计)

写代码之前 最近代码写慢了&#xff0c;磨了好久都没开始动手写代码。考虑的东西越多越多&#xff0c;甚至自己都认为过虑了。就像这个程序&#xff0c;写代码之前估计花了大半天或者一天在思考怎么写&#xff0c;不知道是好事还是年纪大了。所以专门写篇文章&#xff0c;把自…

为什么要选择 Redis?

文章目录前言一、选型二、协议三、客户端1、常见 java 客户端2、常见可视化工具&#xff1a;四、Redis 生态1、模块2、代理3、其他前言 Redis&#xff08;Remote Dictionary Server&#xff09;&#xff0c;即「远程字典服务」是一个使用 ANSI C 编写的、开源的、支持网络的、…

【电力系统】基于YALMIP 的微网(光伏+风电+蓄电池+微电网+柴油机)优化调度模型附matlab代码

✅作者简介&#xff1a;热爱科研的Matlab仿真开发者&#xff0c;修心和技术同步精进&#xff0c;matlab项目合作可私信。 &#x1f34e;个人主页&#xff1a;Matlab科研工作室 &#x1f34a;个人信条&#xff1a;格物致知。 更多Matlab仿真内容点击&#x1f447; 智能优化算法 …

Unity—UGUI

每日一句&#xff1a;读数、学习 去更远的地方&#xff0c;才能摆脱那些你不屑一顾的圈子 目录 InputFiled输入框 例&#xff1a;用户名和密码 Toggle组件 案例&#xff1a;冷却效果 InputFiled输入框 Text Component 输入文本组件 Text输入内容 Character Limit 输入字符…

宝塔后渗透-添加用户_反弹shell

更新时间&#xff1a;2022年11月21日 1. 背景介绍 对于想拿到bt后台来说&#xff0c;非常的艰难&#xff1a;无非是通过bypass之后提权&#xff0c;直接拿到服务器的root权限&#xff0c;然后再去宝塔后台。 当然&#xff0c;还有一种运气十分爆棚的方法&#xff1a;发现了b…

Qt的Q_UNUSED()函数的功能

目录Qt Assistant&#xff08;Qt 助手&#xff09;构建场景其他一些平替方法参考Qt Assistant&#xff08;Qt 助手&#xff09; 函数名直译过来是【不用的&#xff1b;从未用过的】。 碰到陌生的函数不要慌&#xff0c;直接Qt Assistant查一哈。 Q_UNUSED(name) Indicates to …