diff --git a/jcommon/docean-plugin/docean-plugin-redisSession/pom.xml b/jcommon/docean-plugin/docean-plugin-redisSession/pom.xml new file mode 100644 index 000000000..00202a3e3 --- /dev/null +++ b/jcommon/docean-plugin/docean-plugin-redisSession/pom.xml @@ -0,0 +1,34 @@ + + + 4.0.0 + + run.mone + docean-plugin + 1.6.0-jdk21-SNAPSHOT + + + docean-plugin-redisSession + + + 21 + 21 + UTF-8 + + + + + run.mone + docean + 1.6.1-jdk21-SNAPSHOT + + + run.mone + docean-plugin-redis + 1.6.0-jdk21-SNAPSHOT + provided + + + + \ No newline at end of file diff --git a/jcommon/docean-plugin/docean-plugin-redisSession/src/main/java/run/mone/docean/plugin/redissession/RedisSessionPlugin.java b/jcommon/docean-plugin/docean-plugin-redisSession/src/main/java/run/mone/docean/plugin/redissession/RedisSessionPlugin.java new file mode 100644 index 000000000..00af04be8 --- /dev/null +++ b/jcommon/docean-plugin/docean-plugin-redisSession/src/main/java/run/mone/docean/plugin/redissession/RedisSessionPlugin.java @@ -0,0 +1,46 @@ +/* + * Copyright 2020 Xiaomi + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package run.mone.docean.plugin.redissession; + +import com.xiaomi.youpin.docean.Ioc; +import com.xiaomi.youpin.docean.anno.DOceanPlugin; +import com.xiaomi.youpin.docean.plugin.IPlugin; +import com.xiaomi.youpin.docean.plugin.redis.Redis; +import lombok.extern.slf4j.Slf4j; + +import java.util.Set; + +/** + * @author shanwb + * @date 2024-09-03 + */ +@DOceanPlugin(order = 101) +@Slf4j +public class RedisSessionPlugin implements IPlugin { + + + @Override + public void init(Set> classSet, Ioc ioc) { + Redis redis = ioc.getBean(Redis.class); + if (null == redis) { + log.error("redis can not be empty"); + } + RedisSessionStore redisSessionStore = new RedisSessionStore(redis); + ioc.putBean("ClusterSessionStore", redisSessionStore); + } + +} diff --git a/jcommon/docean-plugin/docean-plugin-redisSession/src/main/java/run/mone/docean/plugin/redissession/RedisSessionStore.java b/jcommon/docean-plugin/docean-plugin-redisSession/src/main/java/run/mone/docean/plugin/redissession/RedisSessionStore.java new file mode 100644 index 000000000..aa2884024 --- /dev/null +++ b/jcommon/docean-plugin/docean-plugin-redisSession/src/main/java/run/mone/docean/plugin/redissession/RedisSessionStore.java @@ -0,0 +1,100 @@ +package run.mone.docean.plugin.redissession; + +import com.google.gson.Gson; +import com.xiaomi.youpin.docean.mvc.session.DefaultHttpSession; +import com.xiaomi.youpin.docean.mvc.session.HttpSession; +import com.xiaomi.youpin.docean.mvc.session.ISessionStore; +import com.xiaomi.youpin.docean.plugin.redis.Redis; +import lombok.extern.slf4j.Slf4j; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author shanwb + * @date 2024-09-03 + */ +@Slf4j +public class RedisSessionStore implements ISessionStore { + + private static final String SESSION_PREFIX = "DOCEAN_SESSION_"; + + private static final int SESSION_EXPIRE_SECONDS = 3600; // 默认60分钟过期 + + private final static Gson gson = new Gson(); + + private Redis redis; + + public RedisSessionStore(Redis redis) { + this.redis = redis; + } + + @Override + public void put(String sessionId, HttpSession session) { + try { + String key = SESSION_PREFIX + sessionId; + String value = gson.toJson(session); + redis.set(key, value, SESSION_EXPIRE_SECONDS); + } catch (Exception e) { + log.error("Error putting session to redis", e); + } + } + + @Override + public void remove(String sessionId) { + try { + String key = SESSION_PREFIX + sessionId; + redis.del(key); + } catch (Exception e) { + log.error("Error removing session from redis", e); + } + } + + @Override + public List sessionIdList() { + return new ArrayList<>(); + } + + @Override + public HttpSession get(String sessionId) { + try { + String key = SESSION_PREFIX + sessionId; + String value = redis.get(key); + if (value == null) { + return null; + } + return gson.fromJson(value, DefaultHttpSession.class); + } catch (Exception e) { + log.error("Error getting session from redis", e); + return null; + } + } + + @Override + public HttpSession getAndRefresh(String sessionId) { + try { + String key = SESSION_PREFIX + sessionId; + String value = redis.get(key); + if (value == null) { + return null; + } + DefaultHttpSession session = gson.fromJson(value, DefaultHttpSession.class); + redis.expire(key, SESSION_EXPIRE_SECONDS); + return session; + } catch (Exception e) { + log.error("Error getting session from redis", e); + return null; + } + } + + @Override + public boolean containsKey(String sessionId) { + try { + String key = SESSION_PREFIX + sessionId; + return redis.exists(key); + } catch (Exception e) { + log.error("Error checking session existence in redis", e); + return false; + } + } +} diff --git a/jcommon/docean-plugin/pom.xml b/jcommon/docean-plugin/pom.xml index da7636cce..5f23cd892 100644 --- a/jcommon/docean-plugin/pom.xml +++ b/jcommon/docean-plugin/pom.xml @@ -55,6 +55,7 @@ docean-plugin-es-antlr4 docean-plugin-storage docean-plugin-junit + docean-plugin-redisSession diff --git a/jcommon/docean/README.md b/jcommon/docean/README.md index 3919b1f98..6246febd6 100644 --- a/jcommon/docean/README.md +++ b/jcommon/docean/README.md @@ -15,4 +15,20 @@ processing, bean initialization, and loading into the container. The plugin package has added extended support for some commonly used dependencies in projects, such as nacos, dubbo, mybatis, and so on. -* rate limited or exceeded quota \ No newline at end of file +* rate limited or exceeded quota + +#Feature + +## session用法 +### 单机session使用 +需要在ioc中加入一个bean[LocalSessionStore.java](src/main/java/com/xiaomi/youpin/docean/mvc/session/impl/LocalSessionStore.java) +ioc init时,添加扫描路径"com.xiaomi.youpin.docean.mvc.session" + +### 分布式事务 +1.工程中引入两个plugin:[docean-plugin-redis](../docean-plugin/docean-plugin-redis)、[docean-plugin-redisSession](../docean-plugin/docean-plugin-redisSession) +2.添加配置,开启分布式配置:ioc.putBean("$cluster-session", "true") + + + + + diff --git a/jcommon/docean/src/main/java/com/xiaomi/youpin/docean/Mvc.java b/jcommon/docean/src/main/java/com/xiaomi/youpin/docean/Mvc.java index 0a73ec97f..9ded97ac9 100644 --- a/jcommon/docean/src/main/java/com/xiaomi/youpin/docean/Mvc.java +++ b/jcommon/docean/src/main/java/com/xiaomi/youpin/docean/Mvc.java @@ -81,17 +81,19 @@ private Mvc(Ioc ioc) { } private void setConfig(Ioc ioc) { - this.mvcConfig.setAllowCross(Boolean.valueOf(ioc.getBean(MvcConst.ALLOW_CROSS_DOMAIN, MvcConst.FALSE))); - this.mvcConfig.setDownload(Boolean.valueOf(ioc.getBean(MvcConst.MVC_DOWNLOAD, MvcConst.FALSE))); - this.mvcConfig.setUseCglib(Boolean.valueOf(ioc.getBean(MvcConst.CGLIB, MvcConst.TRUE))); + this.mvcConfig.setAllowCross(Boolean.parseBoolean(ioc.getBean(MvcConst.ALLOW_CROSS_DOMAIN, MvcConst.FALSE))); + this.mvcConfig.setDownload(Boolean.parseBoolean(ioc.getBean(MvcConst.MVC_DOWNLOAD, MvcConst.FALSE))); + this.mvcConfig.setUseCglib(Boolean.parseBoolean(ioc.getBean(MvcConst.CGLIB, MvcConst.TRUE))); - this.mvcConfig.setOpenStaticFile(Boolean.valueOf(ioc.getBean(MvcConst.OPEN_STATIC_FILE, MvcConst.FALSE))); + this.mvcConfig.setOpenStaticFile(Boolean.parseBoolean(ioc.getBean(MvcConst.OPEN_STATIC_FILE, MvcConst.FALSE))); this.mvcConfig.setStaticFilePath(ioc.getBean(MvcConst.STATIC_FILE_PATH, MvcConst.EMPTY)); - this.mvcConfig.setResponseOriginalValue(Boolean.valueOf(ioc.getBean(MvcConst.RESPONSE_ORIGINAL_VALUE, MvcConst.FALSE))); + this.mvcConfig.setResponseOriginalValue(Boolean.parseBoolean(ioc.getBean(MvcConst.RESPONSE_ORIGINAL_VALUE, MvcConst.FALSE))); this.mvcConfig.setPoolSize(Integer.valueOf(ioc.getBean(MvcConst.MVC_POOL_SIZE, String.valueOf(MvcConst.DEFAULT_MVC_POOL_SIZE)))); - this.mvcConfig.setVirtualThread(Boolean.valueOf(ioc.getBean(MvcConst.VIRTUAL_THREAD, MvcConst.TRUE))); + this.mvcConfig.setVirtualThread(Boolean.parseBoolean(ioc.getBean(MvcConst.VIRTUAL_THREAD, MvcConst.TRUE))); this.mvcConfig.setResponseOriginalPath(ioc.getBean(MvcConst.RESPONSE_ORIGINAL_PATH, "")); + this.mvcConfig.setClusterSession(Boolean.parseBoolean(ioc.getBean(MvcConst.CLUSTER_SESSION, MvcConst.FALSE))); + ioc.publishEvent(new Event(EventType.mvcBegin, this.mvcConfig)); } diff --git a/jcommon/docean/src/main/java/com/xiaomi/youpin/docean/bo/MvcConfig.java b/jcommon/docean/src/main/java/com/xiaomi/youpin/docean/bo/MvcConfig.java index 89b9741bc..eece34154 100644 --- a/jcommon/docean/src/main/java/com/xiaomi/youpin/docean/bo/MvcConfig.java +++ b/jcommon/docean/src/main/java/com/xiaomi/youpin/docean/bo/MvcConfig.java @@ -47,4 +47,6 @@ public class MvcConfig implements Serializable { */ private String responseOriginalPath; + private boolean clusterSession; + } diff --git a/jcommon/docean/src/main/java/com/xiaomi/youpin/docean/mvc/MvcContext.java b/jcommon/docean/src/main/java/com/xiaomi/youpin/docean/mvc/MvcContext.java index a05a23d17..9e048ab5c 100644 --- a/jcommon/docean/src/main/java/com/xiaomi/youpin/docean/mvc/MvcContext.java +++ b/jcommon/docean/src/main/java/com/xiaomi/youpin/docean/mvc/MvcContext.java @@ -70,6 +70,11 @@ public class MvcContext { */ private boolean allowCross; + /** + * cluster session + */ + private boolean clusterSession; + public HttpSession session() { if (null == session) { this.session = HttpSessionManager.getSession(this); diff --git a/jcommon/docean/src/main/java/com/xiaomi/youpin/docean/mvc/MvcRunnable.java b/jcommon/docean/src/main/java/com/xiaomi/youpin/docean/mvc/MvcRunnable.java index 70242831e..5ef9219c6 100644 --- a/jcommon/docean/src/main/java/com/xiaomi/youpin/docean/mvc/MvcRunnable.java +++ b/jcommon/docean/src/main/java/com/xiaomi/youpin/docean/mvc/MvcRunnable.java @@ -69,6 +69,7 @@ public MvcRunnable(Mvc mvc, HttpServerConfig config, ChannelHandlerContext ctx, this.context.setVirtualThread(mvc.getMvcConfig().isVirtualThread()); this.context.setPath(path); this.context.setAllowCross(mvc.getMvcConfig().isAllowCross()); + this.context.setClusterSession(mvc.getMvcConfig().isClusterSession()); this.request.setMethod(method); this.request.setPath(path); this.request.setBody(body); diff --git a/jcommon/docean/src/main/java/com/xiaomi/youpin/docean/mvc/common/MvcConst.java b/jcommon/docean/src/main/java/com/xiaomi/youpin/docean/mvc/common/MvcConst.java index dd565822c..2c19debbe 100644 --- a/jcommon/docean/src/main/java/com/xiaomi/youpin/docean/mvc/common/MvcConst.java +++ b/jcommon/docean/src/main/java/com/xiaomi/youpin/docean/mvc/common/MvcConst.java @@ -32,6 +32,8 @@ public abstract class MvcConst { public static final String RESPONSE_ORIGINAL_PATH = "$response-original-path"; + public static final String CLUSTER_SESSION = "$cluster-session"; + public static final int DEFAULT_MVC_POOL_SIZE = 200; public static ScopedValue MVC_CONTEXT = ScopedValue.newInstance(); diff --git a/jcommon/docean/src/main/java/com/xiaomi/youpin/docean/mvc/session/HttpSessionManager.java b/jcommon/docean/src/main/java/com/xiaomi/youpin/docean/mvc/session/HttpSessionManager.java index 2c8758044..759908350 100644 --- a/jcommon/docean/src/main/java/com/xiaomi/youpin/docean/mvc/session/HttpSessionManager.java +++ b/jcommon/docean/src/main/java/com/xiaomi/youpin/docean/mvc/session/HttpSessionManager.java @@ -16,7 +16,7 @@ package com.xiaomi.youpin.docean.mvc.session; -import com.xiaomi.youpin.docean.common.Safe; +import com.xiaomi.youpin.docean.Ioc; import com.xiaomi.youpin.docean.mvc.MvcContext; import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.HttpHeaderNames; @@ -26,49 +26,42 @@ import io.netty.handler.codec.http.cookie.ServerCookieEncoder; import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; /** * @author goodjava@qq.com */ public class HttpSessionManager { + private static ISessionStore getSessionStoreService(MvcContext mvcContext) { + boolean clusterSession = false; + if (null != mvcContext) { + clusterSession = mvcContext.isClusterSession(); + } - private static final Map SESSION_MAP = new ConcurrentHashMap<>(); - - - static { - Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(() -> { - Safe.run(() -> { - long now = System.currentTimeMillis(); - List ids = SESSION_MAP.values().stream().filter(it -> { - DefaultHttpSession hs = (DefaultHttpSession) it; - if (now - hs.getUpdateTime() > TimeUnit.MINUTES.toMillis(60)) { - return true; - } - return false; - }).map(it -> it.getId()).collect(Collectors.toList()); - ids.forEach(id -> SESSION_MAP.remove(id)); - }, e -> { - }); - }, 10, 5, TimeUnit.SECONDS); + String beanName = clusterSession ? "ClusterSessionStore" : "SessionStore"; + return (ISessionStore) Ioc.ins().getBean(beanName); } + @Deprecated public static String createSession() { + return createSession(null); + } + + public static String createSession(MvcContext mvcContext) { HttpSession session = new DefaultHttpSession(); String sessionId = session.getId(); - SESSION_MAP.put(sessionId, session); + ISessionStore sessionStoreService = getSessionStoreService(mvcContext); + sessionStoreService.put(sessionId, session); + return sessionId; } public static boolean isExists(String sessionId) { - if (SESSION_MAP.containsKey(sessionId)) { - HttpSession session = SESSION_MAP.get(sessionId); + ISessionStore sessionStoreService = getSessionStoreService(null); + if (sessionStoreService.containsKey(sessionId)) { + HttpSession session = sessionStoreService.get(sessionId); if (session.getId() == null) { - SESSION_MAP.remove(sessionId); + sessionStoreService.remove(sessionId); return false; } return true; @@ -78,32 +71,28 @@ public static boolean isExists(String sessionId) { } public static void invalidate(String sessionId) { - SESSION_MAP.remove(sessionId); + ISessionStore sessionStoreService = getSessionStoreService(null); + sessionStoreService.remove(sessionId); } + @Deprecated public static HttpSession getSession(String sessionId) { - return SESSION_MAP.compute(sessionId, (k, v) -> { - if (null != v) { - if (v instanceof DefaultHttpSession dhs) { - dhs.setUpdateTime(System.currentTimeMillis()); - } - } - return v; - }); + return getSessionStoreService(null).get(sessionId); } public static HttpSession getSession(MvcContext context) { String sid = getSessionId(context.getHeaders()); + ISessionStore sessionStoreService = getSessionStoreService(context); if (sid.equals("")) { - String session = createSession(); - context.setSessionId(session); - return getSession(session); + String sessionId = createSession(context); + context.setSessionId(sessionId); + return sessionStoreService.getAndRefresh(sessionId); } else { return Optional.ofNullable(getSession(sid)).orElseGet(() -> { - String session = createSession(); - context.setSessionId(session); - return getSession(session); + String sessionId = createSession(context); + context.setSessionId(sessionId); + return sessionStoreService.getAndRefresh(sessionId); }); } @@ -120,7 +109,7 @@ public static void setSessionId(MvcContext context, boolean exists, FullHttpResp } if (exists == false) { - Cookie cookie = new DefaultCookie(HttpSession.SESSIONID, HttpSessionManager.createSession()); + Cookie cookie = new DefaultCookie(HttpSession.SESSIONID, HttpSessionManager.createSession(context)); cookie.setPath("/"); String encodeCookie = ServerCookieEncoder.STRICT.encode(cookie); response.headers().set(HttpHeaderNames.SET_COOKIE, encodeCookie); diff --git a/jcommon/docean/src/main/java/com/xiaomi/youpin/docean/mvc/session/ISessionStore.java b/jcommon/docean/src/main/java/com/xiaomi/youpin/docean/mvc/session/ISessionStore.java new file mode 100644 index 000000000..37ca14631 --- /dev/null +++ b/jcommon/docean/src/main/java/com/xiaomi/youpin/docean/mvc/session/ISessionStore.java @@ -0,0 +1,23 @@ +package com.xiaomi.youpin.docean.mvc.session; + +import java.util.List; + +/** + * @author shanwb + * @date 2024-09-03 + */ +public interface ISessionStore { + + void put(String sessionId, HttpSession session); + + void remove(String sessionId); + + List sessionIdList(); + + HttpSession get(String sessionId); + + HttpSession getAndRefresh(String sessionId); + + boolean containsKey(String sessionId); + +} diff --git a/jcommon/docean/src/main/java/com/xiaomi/youpin/docean/mvc/session/impl/LocalSessionStore.java b/jcommon/docean/src/main/java/com/xiaomi/youpin/docean/mvc/session/impl/LocalSessionStore.java new file mode 100644 index 000000000..583e84e6a --- /dev/null +++ b/jcommon/docean/src/main/java/com/xiaomi/youpin/docean/mvc/session/impl/LocalSessionStore.java @@ -0,0 +1,78 @@ +package com.xiaomi.youpin.docean.mvc.session.impl; + +import com.xiaomi.youpin.docean.anno.Component; +import com.xiaomi.youpin.docean.common.Safe; +import com.xiaomi.youpin.docean.mvc.session.DefaultHttpSession; +import com.xiaomi.youpin.docean.mvc.session.HttpSession; +import com.xiaomi.youpin.docean.mvc.session.ISessionStore; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +/** + * @author shanwb + * @date 2024-09-03 + */ +@Component(name = "SessionStore") +public class LocalSessionStore implements ISessionStore { + + private static final Map SESSION_MAP = new ConcurrentHashMap<>(); + + static { + Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(() -> { + Safe.run(() -> { + long now = System.currentTimeMillis(); + List ids = SESSION_MAP.values().stream().filter(it -> { + DefaultHttpSession hs = (DefaultHttpSession) it; + if (now - hs.getUpdateTime() > TimeUnit.MINUTES.toMillis(60)) { + return true; + } + return false; + }).map(HttpSession::getId).toList(); + ids.forEach(SESSION_MAP::remove); + }, e -> { + }); + }, 10, 5, TimeUnit.SECONDS); + } + + @Override + public void put(String sessionId, HttpSession session) { + SESSION_MAP.put(sessionId, session); + } + + @Override + public void remove(String sessionId) { + SESSION_MAP.remove(sessionId); + } + + @Override + public List sessionIdList() { + return new ArrayList<>(SESSION_MAP.keySet()); + } + + @Override + public HttpSession get(String sessionId) { + return SESSION_MAP.get(sessionId); + } + + @Override + public HttpSession getAndRefresh(String sessionId) { + return SESSION_MAP.compute(sessionId, (k, v) -> { + if (null != v) { + if (v instanceof DefaultHttpSession dhs) { + dhs.setUpdateTime(System.currentTimeMillis()); + } + } + return v; + }); + } + + @Override + public boolean containsKey(String sessionId) { + return SESSION_MAP.containsKey(sessionId); + } +} diff --git a/jcommon/health/pom.xml b/jcommon/health/pom.xml index af6c9da68..b6acecc7b 100644 --- a/jcommon/health/pom.xml +++ b/jcommon/health/pom.xml @@ -35,7 +35,7 @@ run.mone aop - 1.6.0-jdk21-SNAPSHOT + 1.6.1-jdk21-SNAPSHOT diff --git a/jcommon/mistarter/pom.xml b/jcommon/mistarter/pom.xml index 55d4f8994..51be0f31b 100644 --- a/jcommon/mistarter/pom.xml +++ b/jcommon/mistarter/pom.xml @@ -13,7 +13,7 @@ run.mone aop - 1.6.0-jdk21-SNAPSHOT + 1.6.1-jdk21-SNAPSHOT org.springframework.boot diff --git a/jcommon/pom.xml b/jcommon/pom.xml index 317b7d665..024f44702 100644 --- a/jcommon/pom.xml +++ b/jcommon/pom.xml @@ -119,7 +119,7 @@ ~/ 8 8 - 1.6.0-jdk21-SNAPSHOT + 1.6.1-jdk21-SNAPSHOT diff --git a/jcommon/schedule/pom.xml b/jcommon/schedule/pom.xml index 48756b12d..e2e51e75b 100644 --- a/jcommon/schedule/pom.xml +++ b/jcommon/schedule/pom.xml @@ -13,13 +13,13 @@ run.mone feishu - 1.6.0-jdk21-SNAPSHOT + 1.6.1-jdk21-SNAPSHOT compile run.mone common - 1.7.1-SNAPSHOT + 1.6.1-jdk21-SNAPSHOT run.mone