Spring Shiro基础组件 Session

相关阅读

简介

会话是在一段时间内和系统交互的Subject的有状态数据上下文;
会话由业务层管理,并可以通过其它层访问,而不需要绑定任何客户端技术;

核心方法

/**
 * 获取id
 */
Serializable getId();

/**
 * 获取创建时间
 */
Date getStartTimestamp();

/**
 * 获取最近一次访问时间
 */
Date getLastAccessTime();

/**
 * 获取超时时间
 */
long getTimeout() throws InvalidSessionException;

/**
 * 设置超时时间
 */
void setTimeout(long maxIdleTimeInMillis) throws InvalidSessionException;

/**
 * 获取创建时所在主机的hostname/ip
 */
String getHost();

/**
 * 显示更新最近一次访问时间为当前时间
 * 避免会话超时
 */
void touch() throws InvalidSessionException;

/**
 * 停止并释放资源
 */
void stop() throws InvalidSessionException;

/**
 * 获取所有属性的键
 */
Collection<Object> getAttributeKeys() throws InvalidSessionException;

/**
 * 获取指定属性
 */
Object getAttribute(Object key) throws InvalidSessionException;

/**
 * 设置指定属性
 */
void setAttribute(Object key, Object value) throws InvalidSessionException;

/**
 * 移除指定属性
 */
Object removeAttribute(Object key) throws InvalidSessionException;

实现子类

public interface Session
    public class DelegatingSession implements Session, Serializable
    public class HttpServletSession implements Session
    public class ProxiedSession implements Session
        public class ImmutableProxiedSession extends ProxiedSession
    public interface ValidatingSession extends Session
        public class SimpleSession implements ValidatingSession, Serializable

DelegatingSession

简介

服务端Session在客户端层的表示;
内部委托服务端的NativeSessionManager的实例实现Session接口;
会适当缓存数据以避免远程方法调用,仅在必要时与服务器通信;

核心方法

// session KEY
private final SessionKey key;
// 客户端层缓存,减少远程方法调用
private Date startTimestamp = null;
private String host = null;
// 内部委托的NativeSessionManager实例
private final transient NativeSessionManager sessionManager;

/**
 * 构造方法
 */
public DelegatingSession(NativeSessionManager sessionManager, SessionKey key) {
    // 校验NativeSessionManager
    if (sessionManager == null) {
        throw new IllegalArgumentException("sessionManager argument cannot be null.");
    }
    // 校验session key
    if (key == null) {
        throw new IllegalArgumentException("sessionKey argument cannot be null.");
    }
    if (key.getSessionId() == null) {
        String msg = "The " + DelegatingSession.class.getName() + " implementation requires that the " +
                "SessionKey argument returns a non-null sessionId to support the " +
                "Session.getId() invocations.";
        throw new IllegalArgumentException(msg);
    }
    this.sessionManager = sessionManager;
    this.key = key;
}

/**
 * 获取id
 */
public Serializable getId() {
    return key.getSessionId();
}

/**
 * 获取创建时间
 */
public Date getStartTimestamp() {
    if (startTimestamp == null) {
        // 缓存创建时间
        startTimestamp = sessionManager.getStartTimestamp(key);
    }
    // 创建时间缓存存在则直接使用
    return startTimestamp;
}

/**
 * 获取最近一次访问时间
 */
public Date getLastAccessTime() {
    //can't cache - only business pojo knows the accurate time:
    return sessionManager.getLastAccessTime(key);
}

/**
 * 获取超时时间
 */
public long getTimeout() throws InvalidSessionException {
    return sessionManager.getTimeout(key);
}

/**
 * 设置超时时间
 */
public void setTimeout(long maxIdleTimeInMillis) throws InvalidSessionException {
    sessionManager.setTimeout(key, maxIdleTimeInMillis);
}

/**
 * 获取创建时所在主机的hostname/ip
 */
public String getHost() {
    if (host == null) {
        // 缓存host信息
        host = sessionManager.getHost(key);
    }
    // host信息缓存存在则直接使用
    return host;
}

/**
 * 显示更新最近一次访问时间为当前时间
 * 避免会话超时
 */
public void touch() throws InvalidSessionException {
    sessionManager.touch(key);
}

/**
 * 停止并释放资源
 */
public void stop() throws InvalidSessionException {
    sessionManager.stop(key);
}

/**
 * 获取所有属性的键
 */
public Collection<Object> getAttributeKeys() throws InvalidSessionException {
    return sessionManager.getAttributeKeys(key);
}

/**
 * 获取指定属性
 */
public Object getAttribute(Object attributeKey) throws InvalidSessionException {
    return sessionManager.getAttribute(this.key, attributeKey);
}

/**
 * 设置指定属性
 */
public void setAttribute(Object attributeKey, Object value) throws InvalidSessionException {
    if (value == null) {
        removeAttribute(attributeKey);
    } else {
        sessionManager.setAttribute(this.key, attributeKey, value);
    }
}

/**
 * 移除指定属性
 */
public Object removeAttribute(Object attributeKey) throws InvalidSessionException {
    return sessionManager.removeAttribute(this.key, attributeKey);
}

HttpServletSession

简介

内部委托标准的Servlet容器的HttpSession实例实现Session接口,而不与Spring Shiro的任何组件交互;

核心方法

// host属性键
private static final String HOST_SESSION_KEY = HttpServletSession.class.getName() +.HOST_SESSION_KEY";
// 更新httpSession时访问的属性键
private static final String TOUCH_OBJECT_SESSION_KEY = HttpServletSession.class.getName() + ".TOUCH_OBJECT_SESSION_KEY";
// 内部委托的HttpSession实例
private HttpSession httpSession = null;

/**
 * 构造方法
 */
public HttpServletSession(HttpSession httpSession, String host) {
    // 校验HttpSession实例
    if (httpSession == null) {
        String msg = "HttpSession constructor argument cannot be null.";
        throw new IllegalArgumentException(msg);
    }
    if (httpSession instanceof ShiroHttpSession) {
        String msg = "HttpSession constructor argument cannot be an instance of ShiroHttpSession.  This " +
                "is enforced to prevent circular dependencies and infinite loops.";
        throw new IllegalArgumentException(msg);
    }
    this.httpSession = httpSession;
    if (StringUtils.hasText(host)) {
        // 设置host信息
        setHost(host);
    }
}

/**
 * 获取id
 */
public Serializable getId() {
    return httpSession.getId();
}

/**
 * 获取创建时间
 */
public Date getStartTimestamp() {
    return new Date(httpSession.getCreationTime());
}

/**
 * 获取最近一次访问时间
 */
public Date getLastAccessTime() {
    return new Date(httpSession.getLastAccessedTime());
}

/**
 * 获取超时时间
 */
public long getTimeout() throws InvalidSessionException {
    try {
        return httpSession.getMaxInactiveInterval() * 1000L;
    } catch (Exception e) {
        throw new InvalidSessionException(e);
    }
}

/**
 * 设置超时时间
 */
public void setTimeout(long maxIdleTimeInMillis) throws InvalidSessionException {
    try {
        int timeout = Long.valueOf(maxIdleTimeInMillis / 1000).intValue();
        httpSession.setMaxInactiveInterval(timeout);
    } catch (Exception e) {
        throw new InvalidSessionException(e);
    }
}

/**
 * 设置所在主机的hostname/ip
 */
protected void setHost(String host) {
    // host信息存在属性集合
    setAttribute(HOST_SESSION_KEY, host);
}

/**
 * 获取创建时所在主机的hostname/ip
 */
public String getHost() {
    return (String) getAttribute(HOST_SESSION_KEY);
}

/**
 * 显示更新最近一次访问时间为当前时间
 * 避免会话超时
 */
public void touch() throws InvalidSessionException {
    //just manipulate the session to update the access time:
    try {
        // 通过操控HttpSession实现更新最近一次访问时间
        httpSession.setAttribute(TOUCH_OBJECT_SESSION_KEY, TOUCH_OBJECT_SESSION_KEY);
        httpSession.removeAttribute(TOUCH_OBJECT_SESSION_KEY);
    } catch (Exception e) {
        throw new InvalidSessionException(e);
    }
}

/**
 * 停止并释放资源
 */
public void stop() throws InvalidSessionException {
    try {
        httpSession.invalidate();
    } catch (Exception e) {
        throw new InvalidSessionException(e);
    }
}

/**
 * 获取所有属性的键
 */
public Collection<Object> getAttributeKeys() throws InvalidSessionException {
    try {
        Enumeration namesEnum = httpSession.getAttributeNames();
        Collection<Object> keys = null;
        if (namesEnum != null) {
            keys = new ArrayList<Object>();
            while (namesEnum.hasMoreElements()) {
                keys.add(namesEnum.nextElement());
            }
        }
        return keys;
    } catch (Exception e) {
        throw new InvalidSessionException(e);
    }
}

/**
 * 校验属性键
 * 只支持String类型键
 */
private static String assertString(Object key) {
    if (!(key instanceof String)) {
        String msg = "HttpSession based implementations of the Shiro Session interface requires attribute keys " +
                "to be String objects.  The HttpSession class does not support anything other than String keys.";
        throw new IllegalArgumentException(msg);
    }
    return (String) key;
}

/**
 * 获取指定属性
 */
public Object getAttribute(Object key) throws InvalidSessionException {
    try {
        return httpSession.getAttribute(assertString(key));
    } catch (Exception e) {
        throw new InvalidSessionException(e);
    }
}

/**
 * 设置指定属性
 */
public void setAttribute(Object key, Object value) throws InvalidSessionException {
    try {
        httpSession.setAttribute(assertString(key), value);
    } catch (Exception e) {
        throw new InvalidSessionException(e);
    }
}

/**
 * 移除指定属性
 */
public Object removeAttribute(Object key) throws InvalidSessionException {
    try {
        String sKey = assertString(key);
        Object removed = httpSession.getAttribute(sKey);
        httpSession.removeAttribute(sKey);
        return removed;
    } catch (Exception e) {
        throw new InvalidSessionException(e);
    }
}

ProxiedSession

简介

代理模式,内部委托被代理的Session实例实现Session接口;

核心方法

// 被代理的Session实例
protected final Session delegate;

/**
 * 构造方法
 */
public ProxiedSession(Session target) {
    // 校验被代理的Session实例
    if (target == null) {
        throw new IllegalArgumentException("Target session to proxy cannot be null.");
    }
    delegate = target;
}

ImmutableProxiedSession

简介

被代理的Session实例不支持任何修改动作;

核心方法

/**
 * 构造方法
 */
public ImmutableProxiedSession(Session target) {
    super(target);
}

/**
 * 抛出修改操作异常
 */
protected void throwImmutableException() throws InvalidSessionException {
    String msg = "This session is immutable and read-only - it cannot be altered.  This is usually because " +
            "the session has been stopped or expired already.";
    throw new InvalidSessionException(msg);
}

/**
 * 设置超时时间
 */
public void setTimeout(long maxIdleTimeInMillis) throws InvalidSessionException {
    throwImmutableException();
}

/**
 * 显示更新最近一次访问时间为当前时间
 * 避免会话超时
 */
public void touch() throws InvalidSessionException {
    throwImmutableException();
}

/**
 * 停止并释放资源
 */
public void stop() throws InvalidSessionException {
    throwImmutableException();
}

/**
 * 设置指定属性
 */
public void setAttribute(Object key, Object value) throws InvalidSessionException {
    throwImmutableException();
}

/**
 * 移除指定属性
 */
public Object removeAttribute(Object key) throws InvalidSessionException {
    throwImmutableException();
    //we should never ever reach this point due to the exception being thrown.
    throw new InternalError("This code should never execute - please report this as a bug!");
}

ValidatingSession

简介

支持校验;
校验通常是确定上次访问或修改会话的时间是否长于指定的允许持续时间;

核心方法

/**
 * 当前会话是否有效
 */
boolean isValid();

/**
 * 校验当前会话
 */
void validate() throws InvalidSessionException;

SimpleSession

简介

ValidatingSession接口的简单实现,用于业务层;
基于JavaBeans的POJO实现Session的接口方法;

核心方法

// session id
private transient Serializable id;
// 创建时间
private transient Date startTimestamp;
// 停止时间
private transient Date stopTimestamp;
// 最近一次访问时间
private transient Date lastAccessTime;
// 超时时间
private transient long timeout;
// 过期标识
private transient boolean expired;
// 主机hostname/ip
private transient String host;
// 属性MAP
private transient Map<Object, Object> attributes;

/**
 * 构造方法
 */
public SimpleSession() {
    // 默认超时时间为全局超时时间
    this.timeout = DefaultSessionManager.DEFAULT_GLOBAL_SESSION_TIMEOUT; //TODO - remove concrete reference to DefaultSessionManager
    // 创建时间为当前时间
    this.startTimestamp = new Date();
    this.lastAccessTime = this.startTimestamp;
}

/**
 * 显示更新最近一次访问时间为当前时间
 * 避免会话超时
 */
public void touch() {
    // 更新最近一次访问时间为当前时间
    this.lastAccessTime = new Date();
}

/**
 * 停止并释放资源
 */
public void stop() {
    if (this.stopTimestamp == null) {
        // 如果还未停止,则设置停止时间为当前时间
        this.stopTimestamp = new Date();
    }
}

/**
 * 当前会话是否已经停止
 */
protected boolean isStopped() {
    // 根据stopTimestamp是否有值进行判断
    return getStopTimestamp() != null;
}

/**
 * 会话过期处理
 */
protected void expire() {
    // 停止会话
    stop();
    // 设置过期标识
    this.expired = true;
}

/**
 * 当前会话是否有效
 */
public boolean isValid() {
    // 会话未停止且未过期
    return !isStopped() && !isExpired();
}

/**
 * 当前会话是否超时
 */
protected boolean isTimedOut() {

    if (isExpired()) {
        // 会话过期,则已超时
        return true;
    }

    // 获取超时时间
    long timeout = getTimeout();

    if (timeout >= 0l) {
        // 获取最近一次访问时间
        Date lastAccessTime = getLastAccessTime();

        if (lastAccessTime == null) {
            String msg = "session.lastAccessTime for session with id [" +
                    getId() + "] is null.  This value must be set at " +
                    "least once, preferably at least upon instantiation.  Please check the " +
                    getClass().getName() + " implementation and ensure " +
                    "this value will be set (perhaps in the constructor?)";
            throw new IllegalStateException(msg);
        }

        // 判断当前时间和最近一次访问时间之间的间隔是否超过超时时间
        // Calculate at what time a session would have been last accessed
        // for it to be expired at this point.  In other words, subtract
        // from the current time the amount of time that a session can
        // be inactive before expiring.  If the session was last accessed
        // before this time, it is expired.
        long expireTimeMillis = System.currentTimeMillis() - timeout;
        Date expireTime = new Date(expireTimeMillis);
        return lastAccessTime.before(expireTime);
    } else {
        if (log.isTraceEnabled()) {
            log.trace("No timeout for session with id [" + getId() +
                    "].  Session is not considered expired.");
        }
    }

    // 当前会话永不超时
    return false;
}

/**
 * 校验当前会话
 */
public void validate() throws InvalidSessionException {
    //check for stopped:
    if (isStopped()) {
        //timestamp is set, so the session is considered stopped:
        String msg = "Session with id [" + getId() + "] has been " +
                "explicitly stopped.  No further interaction under this session is " +
                "allowed.";
        throw new StoppedSessionException(msg);
    }

    // 检查是否超时
    //check for expiration
    if (isTimedOut()) {
        // 超时则执行过期处理
        expire();

        // 组装会话过期细节信息
        //throw an exception explaining details of why it expired:
        Date lastAccessTime = getLastAccessTime();
        long timeout = getTimeout();

        Serializable sessionId = getId();

        DateFormat df = DateFormat.getInstance();
        String msg = "Session with id [" + sessionId + "] has expired. " +
                "Last access time: " + df.format(lastAccessTime) +
                ".  Current time: " + df.format(new Date()) +
                ".  Session timeout is set to " + timeout / MILLIS_PER_SECOND + " seconds (" +
                timeout / MILLIS_PER_MINUTE + " minutes)";
        if (log.isTraceEnabled()) {
            log.trace(msg);
        }
        // 抛出超时异常
        throw new ExpiredSessionException(msg);
    }
}

Session创建流程

Shiro中通过Subject.getSession()方法创建Session,其子类DelegatingSubject实现代码如下:

public Session getSession() {
    // 获取Session,若不存在支持创建
    return getSession(true);
}

public Session getSession(boolean create) {
    if (log.isTraceEnabled()) {
        log.trace("attempting to get session; create = " + create +
                "; session is null = " + (this.session == null) +
                "; session has id = " + (this.session != null && session.getId() != null));
    }

    if (this.session == null && create) {
        // Session还未创建且支持创建

        if (!isSessionCreationEnabled()) {
            String msg = "Session creation has been disabled for the current subject.  This exception indicates " +
                    "that there is either a programming error (using a session when it should never be " +
                    "used) or that Shiro's configuration needs to be adjusted to allow Sessions to be created " +
                    "for the current Subject.  See the " + DisabledSessionException.class.getName() + " JavaDoc " +
                    "for more.";
            throw new DisabledSessionException(msg);
        }

        log.trace("Starting session for host {}", getHost());
        SessionContext sessionContext = createSessionContext();
        // 创建Session
        // 实际是DelegatingSession
        Session session = this.securityManager.start(sessionContext);
        // 装饰Session为StoppingAwareProxiedSession
        this.session = decorate(session);
    }
    return this.session;
}

protected Session decorate(Session session) {
    if (session == null) {
        throw new IllegalArgumentException("session cannot be null");
    }
    // 装饰为StoppingAwareProxiedSession
    return new StoppingAwareProxiedSession(session, this);
}

使用内部的SecurityManager实例,其start方法由父类SessionsSecurityManager实现,代码如下:

public Session start(SessionContext context) throws AuthorizationException {
    if (sessionManager == null) {
        throw new IllegalStateException("Session manager is not available or has been destroyed");
    }
    return this.sessionManager.start(context);
}

使用内部的SessionManager实例,用户可自定义SessionManager实现,否则默认使用DefaultWebSessionManager,配置见AbstractShiroWebConfiguration.nativeSessionManagerDefaultWebSessionManager.start方法继承自父类AbstractNativeSessionManager,代码如下:

public Session start(SessionContext context) {
    // 最终默认由SimpleSessionFactory创建出SimpleSession
    Session session = createSession(context);
    // 应用全局超时时间
    applyGlobalSessionTimeout(session);
    // Session创建时处理
    onStart(session, context);
    // 通知Session创建事件
    notifyStart(session);
    // 创建暴露的Session
    // 只对外暴露SessionId,细节需要借助NativeSessionManager获取
    return createExposedSession(session, context);
}

protected Session createExposedSession(Session session, SessionContext context) {
    // 创建DelegatingSession,通过SessionId查找对应的Session
    return new DelegatingSession(this, new DefaultSessionKey(session.getId()));
}

// DefaultWebSessionManager.java
protected Session createExposedSession(Session session, SessionContext context) {
    if (!WebUtils.isWeb(context)) {
        return super.createExposedSession(session, context);
    }
    ServletRequest request = WebUtils.getRequest(context);
    ServletResponse response = WebUtils.getResponse(context);
    SessionKey key = new WebSessionKey(session.getId(), request, response);
    // 使用WebSessionKey作为SessionKey
    return new DelegatingSession(this, key);
}

由上文可知,Shiro中Subject使用的Session其实是:StoppingAwareProxiedSession -> DelegatingSession -> SimpleSession

Session获取属性流程

由上文可知,Shiro中和Subject管理Session其实为StoppingAwareProxiedSession,分析其getAttribute方法的实现;
StoppingAwareProxiedSessiongetAttribute方法继承自ProxiedSession,代码如下:

public Object getAttribute(Object key) throws InvalidSessionException {
    return delegate.getAttribute(key);
}

其中delegateDelegatingSession,继续分析其实现,代码如下:

public Object getAttribute(Object attributeKey) throws InvalidSessionException {
    return sessionManager.getAttribute(this.key, attributeKey);
}

DelegatingSession内部使用SessionManager封装了真正的Session细节的访问,对外提供的入口就是SessionKeySessionManager默认使用DefaultWebSessionManager(用户可自定义实现),其getAttribute继承自父类AbstractNativeSessionManager,代码如下:

public Object getAttribute(SessionKey sessionKey, Object attributeKey) throws InvalidSessionException {
    return lookupRequiredSession(sessionKey).getAttribute(attributeKey);
}

private Session lookupRequiredSession(SessionKey key) throws SessionException {
    // 根据SessionKey查找对应的Session
    Session session = lookupSession(key);
    if (session == null) {
        // 如果不存在,则抛出异常
        String msg = "Unable to locate required Session instance based on SessionKey [" + key + "].";
        throw new UnknownSessionException(msg);
    }
    return session;
}

private Session lookupSession(SessionKey key) throws SessionException {
    // 校验参数
    if (key == null) {
        throw new NullPointerException("SessionKey argument cannot be null.");
    }
    // 算法细节由子类实现
    return doGetSession(key);
}

doGetSession方法由子类AbstractValidatingSessionManager实现算法模板,增加了校验功能,代码如下:

protected final Session doGetSession(final SessionKey key) throws InvalidSessionException {
    // 确保开启Session校验如果需要的话
    enableSessionValidationIfNecessary();

    log.trace("Attempting to retrieve session with key {}", key);

    // 根据SessionKey查找Session
    Session s = retrieveSession(key);
    if (s != null) {
        // 查找到Session后进一步做校验
        validate(s, key);
    }
    return s;
}

算法细节retrieveSession由子类DefaultSessionManager实现,代码如下:

protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {
    // 从SessionKey中获取SessionId
    Serializable sessionId = getSessionId(sessionKey);
    if (sessionId == null) {
        // 不存在SessionId则直接返回null
        log.debug("Unable to resolve session ID from SessionKey [{}].  Returning null to indicate a " +
                "session could not be found.", sessionKey);
        return null;
    }
    // 根据SessionId从数据库中查找
    Session s = retrieveSessionFromDataSource(sessionId);
    if (s == null) {
        // 找不到则抛出异常
        String msg = "Could not find session with ID [" + sessionId + "]";
        throw new UnknownSessionException(msg);
    }
    return s;
}

protected Serializable getSessionId(SessionKey sessionKey) {
    return sessionKey.getSessionId();
}

protected Session retrieveSessionFromDataSource(Serializable sessionId) throws UnknownSessionException {
    // 使用SessionDao查找Session
    return sessionDAO.readSession(sessionId);
}

// DefaultWebSessionManager.java
public Serializable getSessionId(SessionKey key) {
    Serializable id = super.getSessionId(key);
    if (id == null && WebUtils.isWeb(key)) {
        ServletRequest request = WebUtils.getRequest(key);
        ServletResponse response = WebUtils.getResponse(key);
        // 尝试从Request/Response中再次获取SessionId
        id = getSessionId(request, response);
    }
    return id;
}

DefaultSessionManager中默认使用MemorySessionDAO,用户可通过setSessionDAO方法设置自定义SessionDao实现,MemorySessionDAO使用内存中的ConcurrentHashMap管理SessionKeySessionIdValueSession

查找到真正的Session后,就可以使用其getAttribute获取到属性值了。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
spring boot 实践学习案例,与其它组件结合如 mybatis、jpa、dubbo、redis、mongodb、memcached、kafka、rabbitmq、activemq、elasticsearch、security、shiro等 #### Spring Boot 版本 - 2.0.3.RELEASE #### 模块说明 - springboot-basic - Spring Boot 基础知识,包括SpringBoot起步、配置详解、aop、filter、拦截器、监听、启动器、全局异常处理、外部Tomcat启动、HTTPS、监控 等。 - springboot-data - Spring Boot 数据库操作,包括SpringJDBC、JPA、Mybatis注解版 & XML版、MongoDB。其中,每个版本都有其对应的多数据源解决方案。 - springboot-caches - Spring Boot 缓存,包括redis、ehcache、spring-cache、memcached、使用redis实现session共享 等。 - springboot-templates - Spring Boot 模板,包括thymeleaf、freemarker、jsp、表单校验 等。 - springboot-docs - Spring Boot 文档生成工具,包括 Swagger、Spring RestDocs - springboot-bussiness - Spring Boot 业务应用,包括 定时任务、上传文件、发送邮件、Doc文档操作 等。 - springboot-ajax - Spring Boot AJAX 跨域,包括 JSONP、Node.js与SpringBoot集成使用反向代理 等。 - springboot-websockets - Spring Boot 使用 Websocket - springboot-webflux - Spring Boot 集成 WebFlux 开发反应式 Web 应用 - springboot-dubbo - Spring Boot 集成 Dubbo 的三种方式 - springboot-search - Spring Boot 集成 搜索引擎,包括 elasticsearch、solr - springboot-mq - Spring Boot 集成 消息队列,包括 kafka、rabbitmq、activemq、rocketmq、redismq - springboot-auth - Spring Boot 权限认证,包括 Apache ShiroSpring Security - springboot-cloud - Spring Cloud 入门,包括 Eureka(服务注册与发现)、Config(配置中心)、Hystrix(断路器)、Bus(消息总线) 等

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值