长期以来,session管理就是企业级Java中的一部分,以致于我们潜意识就认为它是已经解决的问题,在最近的记忆中,我们没有看到这个领域有很大的革新。
但是,现代的趋势是微服务以及可水平扩展的原生云应用(cloud native application),它们会挑战过去20多年来我们设计和构建session管理器时的前提假设,并且暴露了现代化session管理器的不足。
本文将会阐述最近发布的Spring Session API如何帮助我们克服眼下session管理方式中的一些不足,在企业级Java中,传统上都会采用这种旧的方式。我们首先会简单阐述一下当前 session管理中的问题,然后深入介绍Spring Session是如何解决这些问题的。在文章的最后,将会详细展示Spring Session是如何运行的,以及在项目中怎样使用它。
相关厂商内容
跟技术大牛,侃侃容器那些事儿!
关于红包、SSD云盘等核心技术集锦!
58到家技术架构快速规划与落地
中国技术开放日上海站:FinTech-技术重定金融,未来大有不同(免费报名)
【双11】QPS从700到1万,唯品会用了这些架构黑魔法
Spring Session为企业级Java应用的session管理带来了革新,使得以下的功能更加容易实现:
- 编写可水平扩展的原生云应用。
- 将session所保存的状态卸载到特定的外部session存储中,如Redis或Apache Geode中,它们能够以独立于应用服务器的方式提供高质量的集群。
- 当用户使用WebSocket发送请求的时候,能够保持HttpSession处于活跃状态。
- 在非Web请求的处理代码中,能够访问session数据,比如在JMS消息的处理代码中。
- 支持每个浏览器上使用多个session,从而能够很容易地构建更加丰富的终端用户体验。
- 控制session id如何在客户端和服务器之间进行交换,这样的话就能很容易地编写Restful API,因为它可以从HTTP 头信息中获取session id,而不必再依赖于cookie。
传统session管理的问题 【通过Spring Session实现新一代的Session管理】传统的JavaEE session管理会有各种问题,这恰好是Spring Session所要试图解决的。这些问题在下面以样例的形式进行了阐述。
构建可水平扩展的原生云应用
在原生的云应用架构中,会假设应用能够进行扩展,这是通过在Linux容器中运行更多的应用程序实例实现的,这些容器会位于一个 大型的虚拟机池中。例如,我们可以很容易地将一个“.war”文件部署到位于Cloud Foundry或Heroku的Tomcat中,然后在几秒钟的时间内就能扩展到100个应用实例,每个实例可以具有1GB RAM。我们还可以配置云平台,基于用户的需求自动增加和减少应用实例的数量。
在很多的应用服务器中,都会将HTTP session状态保存在JVM中,这个JVM与运行应用程序代码的JVM是同一个,因为这样易于实现,并且速度很快。当新的应用服务器实例加入或离开集 群时,HTTP session会基于现有的应用服务器实例进行重新平衡。在弹性的云环境中,我们会拥有上百个应用服务器实例,并且实例的数量可能在任意时刻增加或减少, 这样的话,我们就会遇到一些问题:
- 重平衡HTTP session可能会成为性能瓶颈。
- 为了存储大量的session,会需要很大的堆空间,这会导致垃圾收集,从而对性能产生负面影响。
- 云基础设施通常会禁止TCP多播(multicast),但是session管理器常常会使用这种机制来发现哪一个应用服务器实例加入或离开了集群。
对于像Tomcat这样的开源服务器,很容易找到session管理器的替代方案,这些替代方案可以使用外部的数据存储,如Redis或 Memcached。但是,这些配置过程可能会比较复杂,而且每种应用服务器都有所差别。对于闭源的产品,如WebSphere和Weblogic,寻找 它们的session管理器替代方案不仅非常困难,在有些时候,甚至是无法实现的。
Spring Session提供了一种独立于应用服务器的方案,这种方案能够在Servlet规范之内配置可插拔的session数据存储,不依赖于任何应用服务器的 特定API。这就意味着Spring Session能够用于实现了servlet规范的所有应用服务器之中(Tomcat、Jetty、 WebSphere、WebLogic、JBoss等),它能够非常便利地在所有应用服务器中以完全相同的方式进行配置。我们还可以选择任意最适应需求的 外部session数据存储。这使得Spring Session成为一个很理想的迁移工具,帮助我们将传统的JavaEE应用转移到云中,使其成为满足 12-factor的应用。
每个用户有多个账号
假设我们在example.com上运行面向公众的Web应用,在这个应用中有些用户会创建多个账号。例如,用户Jeff Lebowski可能会有两个账户thedude@example.com和lebowski@example.com。和其他Java Web应用一样,我们会使用
HttpSession
来跟踪应用的状态,如当前登录的用户。所以,当用户希望从thedude@example.com切换到lebowski@example.com时,他必须要首先退出,然后再重新登录回来。借助Spring Session,为每个用户配置多个HTTP session会非常容易,这样用户在thedude@example.com和lebowski@example.com之间切换的时候,就不需要退出和重新登录了。
多级别的安全预览
假设我们正在构建的Web应用有一个复杂、自定义的权限功能,其中应用的UI会基于用户所授予的角色和权限实现自适应。
例如,假设应用有四个安全级别:public、confidential、secret和top secret。当用户登录应用之后,系统会判断用户所具有的最高安全级别并且只会显示该级别和该级别之下的数据。所以,具有public权限的用户只能看 到public级别的文档,具有secret权限的用户能够看到public、confidential和secret级别的文档,诸如此类。为了保证用 户界面更加友好,应用程序应该允许用户预览在较低的安全级别条件下页面是什么样子的。例如,top secret权限的用户能够将应用从top secret模式切换到secret模式,这样就能站在具有secret权限用户的视角上,查看应用是什么样子的。
典型的Web应用会将当前用户的标识及其角色保存在HTTP session中,但因为在Web应用中,每个登录的用户只能有一个session,因此除了用户退出并重新登录进来,我们并没有办法在角色之间进行切换,除非我们为每个用户自行实现多个session的功能。
借助Spring Session,可以很容易地为每个登录用户创建多个session,这些session之间是完全独立的,因此实现上述的预览功能是非常容易的。例如, 当前用户以top secret角色进行了登录,那么应用可以创建一个新的session,这个session的最高安全角色是secret而不是top secret,这样的话,用户就可以在secret模式预览应用了。
当使用Web Socket的时候保持登录状态
假设用户登录了example.com上的Web应用,那么他们可以使用HTML5的chat客户端实现聊天的功能,这个客户端构建在 websocket之上。按照servlet规范,通过websocket传入的请求并不能保持HTTP session处于活跃状态,所以当用户在聊天的过程中,HTTP session的倒数计时器会在不断地流逝。即便站在用户的立场上,他们一直在使用应用程序,HTTP session最终也可能会出现过期。当HTTP session过期时,websocket连接将会关闭。
借助Spring Session,对于系统中的用户,我们能够很容易地实现websocket请求和常规的HTTP请求都能保持HTTP session处于活跃状态。
非Web请求访问Session数据 假设我们的应用提供了两种访问方式:一种使用基于HTTP的REST API,而另一种使用基于RabbitMQ的AMQP消息。执行消息处理代码的线程将无法访问应用服务器的HttpSession,所以我们必须要以一种 自定义的方案来获取HTTP session中的数据,这要通过自定义的机制来实现。
通过使用Spring Session,只要我们能够知道session的id,就可以在应用的任意线程中访问Spring Session。因此,Spring Session具备比Servlet HTTP session管理器更为丰富的API,只要知道了session id,我们就能获取任意特定的session。例如,在一个传入的消息中可能会包含用户id的header信息,借助它,我们就可以直接获取 session了。
Spring Session是如何运行的 我们已经讨论了在传统的应用服务器中,HTTP session管理存在不足的各种场景,接下来看一下Spring Session是如何解决这些问题的。
Spring Session的架构
当实现session管理器的时候,有两个必须要解决的核心问题。首先,如何创建集群环境下高可用的session,要求能够可靠并高效地存储数 据。其次,不管请求是HTTP、WebSocket、AMQP还是其他的协议,对于传入的请求该如何确定该用哪个session实例。实质上,关键问题在 于:在发起请求的协议上,session id该如何进行传输?
Spring Session认为第一个问题,也就是在高可用可扩展的集群中存储数据已经通过各种数据存储方案得到了解决,如Redis、GemFire以及 Apache Geode等等,因此,Spring Session定义了一组标准的接口,可以通过实现这些接口间接访问底层的数据存储。Spring Session定义了如下核心接口:
Session、ExpiringSession
以及SessionRepository
,针对不同的数据存储,它们需要分别实现。org.springframework.session.Session
接口定义了session的基本功能,如设置和移除属性。这个接口并不关心底层技术,因此能够比servlet HttpSession适用于更为广泛的场景中。org.springframework.session.ExpiringSession
扩展了Session接口,它提供了判断session是否过期的属性。RedisSession是这个接口的一个样例实现。org.springframework.session.SessionRepository
定义了创建、保存、 删除以及检索session的方法。将Session实例真正保存到数据存储的逻辑是在这个接口的实现中编码完成的。例 如,RedisOperationsSessionRepository就是这个接口的一个实现,它会在Redis中创建、存储和删除session。
对于HTTP协议来说,Spring Session定义了
HttpSessionStrategy
接口以及两个默认实现,即CookieHttpSessionStrategy
和HeaderHttpSessionStrategy
,其中前者使用HTTP cookie将请求与session id关联,而后者使用HTTP header将请求与session关联。如下的章节详细阐述了Spring Session使用HTTP协议的细节。
在撰写本文的时候,在当前的Spring Session 1.0.2 GA发布版本中,包含了Spring Session使用Redis的实现,以及基于Map的实现,这个实现支持任意的分布式Map,如Hazelcast。让Spring Session支持某种数据存储是相当容易的,现在有支持各种数据存储的社区实现。
Spring Session对HTTP的支持
Spring Session对HTTP的支持是通过标准的servlet filter来实现的,这个filter必须要配置为拦截所有的web应用请求,并且它应该是filter链中的第一个filter。Spring Session filter会确保随后调用
javax.servlet.http.HttpServletRequest
的getSession()
方法时,都会返回Spring Session的HttpSession
实例,而不是应用服务器默认的HttpSession。如果要理解它的话,最简单的方式就是查看Spring Session实际所使用的源码。首先,我们了解一下标准servlet扩展点的一些背景知识,在实现Spring Session的时候会使用这些知识。
在2001年,Servlet 2.3规范引入了
ServletRequestWrapper
。它的javadoc文档这样写道,ServletRequestWrapper
“提供了ServletRequest
接 口的便利实现,开发人员如果希望将请求适配到Servlet的话,可以编写它的子类。这个类实现了包装(Wrapper)或者说是装饰 (Decorator)模式。对方法的调用默认会通过包装的请求对象来执行”。如下的代码样例抽取自Tomcat,展现了 ServletRequestWrapper是如何实现的。public class ServletRequestWrapper implements ServletRequest {private ServletRequest request;
/**
* 创建ServletRequest适配器,它包装了给定的请求对象。
* @throws java.lang.IllegalArgumentException if the request is null
*/
public ServletRequestWrapper(ServletRequest request) {
if (request == null) {
throw new IllegalArgumentException("Request cannot be null");
}
this.request = request;
}public ServletRequest getRequest() {
return this.request;
}public Object getAttribute(String name) {
return this.request.getAttribute(name);
}// 为了保证可读性,其他的方法删减掉了
}
Servlet 2.3规范还定义了
HttpServletRequestWrapper
,它是ServletRequestWrapper
的子类,能够快速提供HttpServletRequest
的自定义实现,如下的代码是从Tomcat抽取出来的,展现了HttpServletRequesWrapper
类是如何运行的。public class HttpServletRequestWrapper extends ServletRequestWrapper
implements HttpServletRequest {public HttpServletRequestWrapper(HttpServletRequest request) {
super(request);
}private HttpServletRequest _getHttpServletRequest() {
return (HttpServletRequest) super.getRequest();
}public HttpSession getSession(boolean create) {
return this._getHttpServletRequest().getSession(create);
}public HttpSession getSession() {
return this._getHttpServletRequest().getSession();
}
// 为了保证可读性,其他的方法删减掉了
}
所以,借助这些包装类就能编写代码来扩展
HttpServletRequest
,重载返回HttpSession
的方法,让它返回由外部存储所提供的实现。如下的代码是从Spring Session项目中提取出来的,但是我将原来的注释替换为我自己的注释,用来在本文中解释代码,所以在阅读下面的代码片段时,请留意注释。/*
* 注意,Spring Session项目定义了扩展自
* 标准HttpServletRequestWrapper的类,用来重载
* HttpServletRequest中与session相关的方法。
*/
private final class SessionRepositoryRequestWrapper
extends HttpServletRequestWrapper {private HttpSessionWrapper currentSession;
private Boolean requestedSessionIdValid;
private boolean requestedSessionInvalidated;
private final HttpServletResponse response;
private final ServletContext servletContext;
/*
* 注意,这个构造器非常简单,它接受稍后会用到的参数,
* 并且委托给它所扩展的HttpServletRequestWrapper
*/
private SessionRepositoryRequestWrapper(
HttpServletRequest request,
HttpServletResponse response,
ServletContext servletContext) {
super(request);
this.response = response;
this.servletContext = servletContext;
}/*
* 在这里,Spring Session项目不再将调用委托给
* 应用服务器,而是实现自己的逻辑,
* 返回由外部数据存储作为支撑的HttpSession实例。
*
* 基本的实现是,先检查是不是已经有session了。如果有的话,
* 就将其返回,否则的话,它会检查当前的请求中是否有session id。
* 如果有的话,将会根据这个session id,从它的SessionRepository中加载session。
* 如果session repository中没有session,或者在当前请求中,
* 没有当前session id与请求关联的话,
* 那么它会创建一个新的session,并将其持久化到session repository中。
*/
@Override
public HttpSession getSession(boolean create) {
if(currentSession != null) {
return currentSession;
}
String requestedSessionId = getRequestedSessionId();
if(requestedSessionId != null) {
S session = sessionRepository.getSession(requestedSessionId);
if(session != null) {
this.requestedSessionIdValid = true;
currentSession = new HttpSessionWrapper(session, getServletContext());
currentSession.setNew(false);
return currentSession;
}
}
if(!create) {
return null;
}
S session = sessionRepository.createSession();
currentSession = new HttpSessionWrapper(session, getServletContext());
return currentSession;
}@Override
public HttpSession getSession() {
return getSession(true);
}
}
Spring Session定义了
SessionRepositoryFilter
,它实现了 Servlet Filter
接口。我抽取了这个filter的关键部分,将其列在下面的代码片段中,我还添加了一些注释,用来在本文中阐述这些代码,所以,同样的,请阅读下面代码的注释部分。/*
* SessionRepositoryFilter只是一个标准的ServletFilter,
* 它的实现扩展了一个helper基类。
*/
public class SessionRepositoryFilter < S extends ExpiringSession >
extends OncePerRequestFilter { /*
* 这个方法是魔力真正发挥作用的地方。这个方法创建了
* 我们上文所述的封装请求对象和
* 一个封装的响应对象,然后调用其余的filter链。
* 这里,关键在于当这个filter后面的应用代码执行时,
* 如果要获得session的话,得到的将会是Spring Session的
* HttpServletSession实例,它是由后端的外部数据存储作为支撑的。
*/
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {request.setAttribute(SESSION_REPOSITORY_ATTR, sessionRepository);
SessionRepositoryRequestWrapper wrappedRequest =
new SessionRepositoryRequestWrapper(request,response,servletContext);
SessionRepositoryResponseWrapper wrappedResponse =
new SessionRepositoryResponseWrapper(wrappedRequest, response);
HttpServletRequest strategyRequest =
httpSessionStrategy.wrapRequest(wrappedRequest, wrappedResponse);
HttpServletResponse strategyResponse =
httpSessionStrategy.wrapResponse(wrappedRequest, wrappedResponse);
try {
filterChain.doFilter(strategyRequest, strategyResponse);
} finally {
wrappedRequest.commitSession();
}
}
}
我们从这一章节得到的关键信息是,Spring Session对HTTP的支持所依靠的是一个简单老式的
ServletFilter
,借助servlet规范中标准的特性来实现Spring Session的功能。因此,我们能够让已有的war文件使用Spring Session的功能,而无需修改已有的代码,当然如果你使用javax.servlet.http.HttpSessionListener
的话,就另当别论了。Spring Session 1.0并不支持HttpSessionListener
,但是Spring Session 1.1 M1发布版本已经添加了对它的支持,你可以通过该地址了解更多细节信息。配置Spring Session
在Web项目中配置Spring Session分为四步:
- 搭建用于Spring Session的数据存储
- 将Spring Session的jar文件添加到web应用中
- 将Spring Session filter添加到web应用的配置中
- 配置Spring Session如何选择session数据存储的连接
有两种常见的方式能够完成上述的Spring Session配置步骤。第一种方式是使用Spring Boot来自动配置Spring Session。第二种配置Spring Session的方式是手动完成上述的每一个配置步骤。
借助像Maven或Gradle这样的依赖管理器,将Spring Session添加应用中是很容易的。如果你使用Maven和Spring Boot的话,那么可以在pom.xml中使用如下的依赖:
org.springframework.session
spring-session
1.0.2.RELEASE
org.springframework.boot
spring-boot-starter-redis
其中,
spring-boot-starter-redis
依赖能够确保使用redis所需的所有jar都会包含在应用中,所以它们可以借助Spring Boot进行自动装配。spring-session依赖将会引入 Spring Session
的jar。至于Spring Session Servlet filter的配置,可以通过Spring Boot的自动配置来实现,这只需要在Spring Boot的配置类上使用
@EnableRedisHttpSession
注解就可以了,如下面的代码片段所示。@SpringBootApplication
@EnableRedisHttpSession
public class ExampleApplication {public static void main(String[] args) {
SpringApplication.run(ExampleApplication.class, args);
}
}
至于Spring Session到Redis连接的配置,可以添加如下配置到Spring Boot的application.properties文件中:
spring.redis.host=localhost
spring.redis.password=secret
spring.redis.port=6379
Spring Boot提供了大量的基础设施用来配置到Redis的连接,定义到Redis数据库连接的各种方式都可以用在这里。你可以参考该地址的逐步操作指南,来了解如何使用Spring Session和Spring Boot。
在传统的web应用中,可以参考该指南来了解如何通过web.xml来使用Spring Session。
在传统的war文件中,可以参考该指南来了解如何不使用web.xml进行配置。
默认情况下,Spring Session会使用HTTP cookie来存储session id,但是我们也可以配置Spring Session使用自定义的HTTP header信息,如
x-auth-token: 0dc1f6e1-c7f1-41ac-8ce2-32b6b3e57aa3
,当构建REST API的时候,这种方式是很有用的。完整的指南可以参考该地址。使用Spring Session
Spring Session配置完成之后,我们就可以使用标准的Servlet API与之交互了。例如,如下的代码定义了一个servlet,它使用标准的Servlet session API来访问session。
@WebServlet("/example")
public class Example extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {// 使用正常的servlet API获取session,在底层,
// session是通过Spring Session得到的,并且会存储到Redis或
// 其他你所选择的数据源中HttpSession session = request.getSession();
String value = https://www.it610.com/article/session.getAttribute(a€?someAttributea€);
}
}
每个浏览器多个Session
Spring Session会为每个用户保留多个session,这是通过使用名为“
_s
”的session别名参数实现的。例如,如果到达的请求为http://example.com/doSomething?_s=0 ,那么Spring Session将会读取“_s”参数的值,并通过它确定这个请求所使用的是默认session。如果到达的请求是http://example.com/doSomething?
_s=1
的话,那么Spring Session就能知道这个请求所要使用的session别名为1.如果请求没有指定“_s
”参数的话,例如http://example.com/doSomething,那么Spring Session将其视为使用默认的session,也就是说_s=0
。要为某个浏览器创建新的session,只需要调用
javax.servlet.http.HttpServletRequest.getSession()
就可以了,就像我们通常所做的那样,Spring Session将会返回正确的session或者按照标准Servlet规范的语义创建一个新的session。下面的表格描述了针对同一个浏览器窗口,getSession()
面对不同url时的行为。 HTTP请求URL |
Session别名 |
getSession()的行为 |
example.com/resource |
0 |
如果存在session与别名0关联的话,就返回该session,否则的话创建一个新的session并将其与别名0关联。 |
example.com/resource?_s=1 |
1 |
如果存在session与别名1关联的话,就返回该session,否则的话创建一个新的session并将其与别名1关联。 |
example.com/resource?_s=0 |
0 |
如果存在session与别名0关联的话,就返回该session,否则的话创建一个新的session并将其与别名0关联。 |
example.com/resource?_s=abc |
abc |
如果存在session与别名abc关联的话,就返回该session,否则的话创建一个新的session并将其与别名abc关联。 |
HttpSessionManager
接口,这个接口包含了一些使用session别名的工具方法。我们可以在
HttpServletRequest
中,通过名为“org.springframework.session.web.http.HttpSessionManager”
的属性获取当前的HttpSessionManager
。如下的样例代码阐述了如何得到HttpSessionManager,并且在样例注释中描述了其关键方法的行为。@WebServlet("/example")
public class Example extends HttpServlet {@Override
protected void doGet(HttpServletRequest request,HttpServletResponse response)
throws ServletException, IOException {/*
* 在请求中,根据名为org.springframework.session.web.http.HttpSessionManager的key
* 获得Spring Session session管理器的引用
*/HttpSessionManager sessionManager=(HttpSessionManager)request.getAttribute(
"org.springframework.session.web.http.HttpSessionManager");
/*
* 使用session管理器找出所请求session的别名。
* 默认情况下,session别名会包含在url中,并且请求参数的名称为“_s”。
* 例如,http://localhost:8080/example?_s=1
* 将会使如下的代码打印出“Requested Session Alias is: 1”
*/
String requestedSessionAlias=sessionManager.getCurrentSessionAlias(request);
System.out.println("Requested Session Alias is:" + requestedSessionAlias);
/* 返回一个唯一的session别名id,这个别名目前没有被浏览器用来发送请求。
* 这个方法并不会创建新的session,
* 我们需要调用request.getSession()来创建新session。
*/
String newSessionAlias = sessionManager.getNewSessionAlias(request);
/* 使用新创建的session别名来建立URL,这个URL将会包含
* “_s”参数。例如,如果newSessionAlias的值为2的话,
* 那么如下的方法将会返回“/inbox?_s=2”
*/String encodedURL = sessionManager.encodeURL("/inbox", newSessionAlias);
System.out.println(encodedURL);
/* 返回session别名与session id所组成的Map,
* 它们是由浏览器发送请求所形成的。
*/
Map < String, String > sessionIds = sessionManager.getSessionIds(request);
}
}
结论 Spring Session为企业级Java的session管理带来了革新,使得如下的任务变得更加容易:
- 编写可水平扩展的原生云应用。
- 将session所保存的状态卸载到特定的外部session存储中,如Redis或Apache Geode中,它们能够以独立于应用服务器的方式提供高质量的集群。
- 当用户使用WebSocket发送请求的时候,能够保持HttpSession处于活跃状态。
- 在非Web请求的处理代码中,能够访问session数据,比如在JMS消息的处理代码中。
- 支持每个浏览器上使用多个session,这样就可以很容易地构建更加丰富的终端用户体验。
- 控制客户端和服务器端之间如何进行session id的交换,这样更加易于编写Restful API,因为它可以从HTTP 头信息中获取session id,而不必再依赖于cookie。
参考资料 Spring Session项目
Spring Session使用指南
- HttpSession & Redis
- Spring Boot集成
- Spring Security集成
- Restful API
- 多用户账号
- Web Socket集成
- ASF Bugzilla – Bug 54738
- WEBSOCKET SPEC-175
关于作者 Adib Saikali是 Pivotal的高级现场工程师(Senior Field Engineer),对技术和创业充满热情,所工作的内容包括组装JavaScript代码,给风险资本家拨打不经过预约的电话等等不一而足。在过去的十 多年中,Adib一直使用Spring和Java构建解决方案,目前致力于帮助客户借助大数据、PaaS以及敏捷方法论的作用,构建优秀的产品和服务。你 可以通过twitter联系到Adib,他的账号是@asaikali。
推荐阅读
- Java|Java基础——数组
- 人工智能|干货!人体姿态估计与运动预测
- java简介|Java是什么(Java能用来干什么?)
- Java|规范的打印日志
- Linux|109 个实用 shell 脚本
- 程序员|【高级Java架构师系统学习】毕业一年萌新的Java大厂面经,最新整理
- Spring注解驱动第十讲--@Autowired使用
- SqlServer|sql server的UPDLOCK、HOLDLOCK试验
- jvm|【JVM】JVM08(java内存模型解析[JMM])
- 技术|为参加2021年蓝桥杯Java软件开发大学B组细心整理常见基础知识、搜索和常用算法解析例题(持续更新...)