在上一篇文章 深入底层,spring mvc父子容器初始化过程解析 中,介绍了Java Web的基础知识,以及Spring MVC父子容器初始化过程,有兴趣的读者可以阅读一下,一是作为本文的铺垫,二是本文所用到的项目也可以从上一篇文章获取到。
本文由上一篇文章引申出来,我们知道Java Web有个Session的概念,是存在于服务端的一块内存,但如今服务都是集群部署,如何解决集群多个节点间session不共享的问题呢?现有如下几种方案:
session共享这种方案实用得多,也是现在最常用的方案。
<dependency>
<groupId>org.springframework.sessiongroupId>
<artifactId>spring-session-coreartifactId>
<version>2.2.2.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframework.sessiongroupId>
<artifactId>spring-session-data-redisartifactId>
<version>2.2.2.RELEASEversion>
dependency>
<dependency>
<groupId>io.lettucegroupId>
<artifactId>lettuce-coreartifactId>
<version>5.2.2.RELEASEversion>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>2.0.21version>
dependency>
spring-session-core是spring session框架核心依赖,spring-session-data-redis依赖则是将session数据持久化到redis中,也可以通过JDBC持久化到关系型数据库中,而lettuce-core则是spring session操作redis服务的客户端依赖,fastjson用于session数据存储到redis时进行序列化。
HttpSession是servlet规范的一个接口,而Spring Session与HttpSession集成之后,会通过过滤器springSessionRepositoryFilter将HttpSession转化为自己的Session,从而可以将session数据写到redis中。
可以通过继承AbstractHttpSessionApplicationInitializer配置springSessionRepositoryFilter过滤器。
import org.springframework.session.web.context.AbstractHttpSessionApplicationInitializer;
/**
* 用于初始化springSessionRepositoryFilter
*/
public class SpringSessionInitializer extends AbstractHttpSessionApplicationInitializer {
public SpringSessionInitializer() {
super();
}
}
Spring Session配置:
import com.alibaba.fastjson2.JSONReader;
import com.alibaba.fastjson2.JSONWriter;
import com.alibaba.fastjson2.support.config.FastJsonConfig;
import com.alibaba.fastjson2.support.spring.data.redis.FastJsonRedisSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.event.EventListener;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.session.Session;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import org.springframework.session.events.SessionCreatedEvent;
import org.springframework.session.events.SessionDeletedEvent;
import org.springframework.session.events.SessionExpiredEvent;
@EnableRedisHttpSession
public class SpringSessionConfig {
@Bean
public LettuceConnectionFactory connectionFactory() {
return new LettuceConnectionFactory();
}
@EventListener
public void onCreated(SessionCreatedEvent event) {
String sessionId = event.getSessionId();
// spring-session提供的session
Session session = event.getSession();
System.out.println("创建:" + sessionId);
}
@EventListener
public void onDeleted(SessionDeletedEvent event) {
String sessionId = event.getSessionId();
// spring-session提供的session
Session session = event.getSession();
System.out.println("删除:" + sessionId);
}
@EventListener
public void onExpired(SessionExpiredEvent event) {
String sessionId = event.getSessionId();
// spring-session提供的session
Session session = event.getSession();
System.out.println("过期:" + sessionId);
}
@Bean(name="springSessionDefaultRedisSerializer")
public RedisSerializer<Object> redisSerializer() {
FastJsonRedisSerializer<Object> serializer = new FastJsonRedisSerializer<>(Object.class);
FastJsonConfig config = new FastJsonConfig();
config.setReaderFeatures(JSONReader.Feature.SupportAutoType);
config.setWriterFeatures(JSONWriter.Feature.WriteClassName);
serializer.setFastJsonConfig(config);
return serializer;
}
}
@EnableRedisHttpSession用于开启以redis为存储媒介的spring session。由于spring session的过滤器会将HttpSession转为自己的session,因此HttpSessionListener也会失效,但spring session会触发自己的session事件,如SessionCreatedEvent,我们可以监听它们。
最后,我们实现一个利用spring session统计访问次数的功能。
ChatController:
import com.bobo.springmvc.service.ChatService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
@Controller
@Slf4j
public class ChatController {
@Autowired
private ChatService chatService;
@RequestMapping(method = RequestMethod.GET,value = "/doChat",produces = "text/plain;charset=utf-8")
@ResponseBody
public String doChat(HttpServletRequest request){
HttpSession session = request.getSession();
if(null == session.getAttribute("viewCount")){
session.setAttribute("viewCount",1);
}else{
session.setAttribute("viewCount",((int)(session.getAttribute("viewCount")))+1);
}
chatService.doChat(request);
return request.getLocalAddr()+":"+request.getLocalPort()+" 访问次数:"+session.getAttribute("viewCount");
}
}
另外为了测试对象序列化,ChatController还注入了ChatService,并调用了doChat方法。
ChatService:
import com.alibaba.fastjson2.JSONObject;
import com.bobo.springmvc.entity.Chat;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
@Service
@Slf4j
public class ChatService implements ApplicationContextAware {
/**
* 测试spring session redis json序列化
*/
public void doChat(HttpServletRequest request){
log.info("开始聊天");
Object attr = request.getSession().getAttribute("nbaChat");
if(null == attr){
Chat chat = new Chat();
chat.setId(1);
chat.setFrom("杜兰特");
chat.setTo("维斯布鲁克");
chat.setMessage("你很棒棒哦");
request.getSession().setAttribute("nbaChat",chat);
}else{
Chat nbaChat = (Chat)attr;
nbaChat.setId(nbaChat.getId()+1);
request.getSession().setAttribute("nbaChat",nbaChat);
}
log.info("聊天内容:{}", JSONObject.toJSONString(request.getSession().getAttribute("nbaChat")));
}
}
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
<script src="jquery-3.4.1.js">script>
<script>
$(function () {
$.ajax({
url: "http://localhost:8070/doChat",
method : "get",
xhrFields : {withCredentials : true},
success:function (result) {
$("#content").text(result);
}
});
});
script>
head>
<body>
<h1 id="content">h1>
body>
html>
部署架构:
因为要测分布式session,所以这里用到了nginx负载均衡,另外将前端静态资源放到一个单独的nginx服务器,实现前后端分离。
这里涉及到跨域问题以及跨域cookie问题,所以需要在负载均衡nginx中添加跨域配置,且前端页面在发起ajax请求时要设置withCredentials为true。
将静态资源文件拷贝到nginx目录下的html/SpringMvc目录下,如下图所示。
然后配置nginx.conf:
# server模块下;root为静态资源存放目录
server {
listen 80;
location / {
root html/SpringMvc;
index index.html index.htm;
}
}
# http模块下
http {
upstream webservers{
server localhost:8081;
server localhost:8082;
}
server {
listen 8070;
add_header 'Access-Control-Allow-Origin' 'http://localhost';
add_header 'Access-Control-Allow-Headers' '*';
add_header 'Access-Control-Allow-Methods' '*';
add_header 'Access-Control-Allow-Credentials' 'true';
if ($request_method = 'OPTIONS') {
return 204;
}
location / {
proxy_pass http://webservers;
}
}
}
这里配置了跨域的CORS响应头,另外设置Access-Control-Allow-Credentials为true是为了发送跨域cookie,如果跨域cookie无法发送那么session计数功能也就实现不了。另外当Access-Control-Allow-Credentials为true时Access-Control-Allow-Origin不允许为*。
按以下顺序依次启动各个服务:
windows杀死所有nginx进程命令:taskkill /f /t /im nginx.exe
redis GUI软件推荐:RedisFront
各服务启动成功之后,浏览器访问http://localhost,并不断刷新页面,可以看到每次访问的服务节点可能不同,访问次数递增,如下图所示。
观察redis中spring session的数据,如下图所示。