Spring+Shiro+Servlet实现轻量未读消息数量推送

17 篇文章 0 订阅
2 篇文章 0 订阅

1. Shiro Session中保存一个bean,用unKnowMsg来记录未读的消息数量

public class UserSessionBean implements Serializable{
    private int memberId=0;
    //消息需要
    private int userId=0;
    private String nickname="guest";
    //假定登录成功后使用的ip不可以再变更
    //操作记录需要
    private String ip;
    //未读的消息数量
    private int unknowMsg=0;
    //ETC
}

1.1 当有新消息时: 看消息的接收者是否在线,若在线,从第二步中取得Session ID,使用Session ID进而取得bean并unKnowMsg +1

1.2 当消息被阅读或删除时: 肯定是在线的,从第二步中取得Session ID,使用Session ID进而取得bean并unKnowMsg -1

1.3 使用异步Servlet来推送bean的unKnowMsg值.代码如下

public class MessagePushServlet extends HttpServlet {
    private final static int DEFAULT_TIME_OUT = 10 * 60 * 1000;
    private final static Logger logger=LoggerFactory.getLogger(MessagePushServlet.class);
    //Shiro Session中保存UserSessionBean的key
    private final String sessionSymbol;

    public MessagePushServlet() {
        //从资源文件中读取Key
        sessionSymbol = ControllerHelper.getGlobalConfig("site.session.symbol");
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        if(sessionSymbol == null){
            resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "not find operate symbol!");
            return;
        }
        //http header
        resp.setStatus(200);
        resp.setContentType("text/event-stream");
        resp.setCharacterEncoding("UTF-8");
        resp.setHeader("Cache-Control","no-cache");
        resp.setHeader("Connection","keep-alive");
        //Shiro
        Subject subject=SecurityUtils.getSubject();
        org.apache.shiro.session.Session shiroSession=subject.getSession();
        final UserSessionBean bean=(UserSessionBean) shiroSession.getAttribute(sessionSymbol);

        final AsyncContext actx = req.startAsync(req, resp);
        actx.setTimeout(DEFAULT_TIME_OUT);
        actx.addListener(new AsyncListener() { //接口负责管理异步事件
            //异步执行完毕时
            @Override
            public void onComplete(AsyncEvent arg0) throws IOException {
                // TODO Auto-generated method stub
                logger.info("[RMS][front]AT query messages");
            }
            //异步线程出错时
            @Override
            public void onError(AsyncEvent arg0) throws IOException {
                // TODO Auto-generated method stub
                logger.info("[RMS][front]AT error");
            }
            //异步线程开始时
            @Override
            public void onStartAsync(AsyncEvent arg0) throws IOException {
                // TODO Auto-generated method stub
                logger.info("[RMS][front]AT start:" + arg0.getSuppliedRequest().getRemoteAddr());
            }
            //异步线程执行超时
            @Override
            public void onTimeout(AsyncEvent arg0) throws IOException {
                // TODO Auto-generated method stub
                logger.info("[RMS][front]AT timelost");
            }
        });
        actx.start(new Runnable(){
            @Override
            public void run() {
                try {
                    PrintWriter out = actx.getResponse().getWriter();
                    //推消息
                    out.println("data:"+ bean.getUnknowMsg() + "\r\n");
                    out.flush();
                    actx.complete();
                    //等待十秒钟
                    Thread.sleep(10000);
                } catch (Exception e) {
                    logger.info("[RMS][front]AT exception:"+e.getMessage());
                }
            }
        });
    }
}

2. 实现org.apache.shiro.session.SessionListener接口,实现在线记录的增删除

/**
 * 在线记录
 * @author xiaofanku@live.cn
 * @since 20170829
 */
@Entity
@Table(name="apo_forum_online", uniqueConstraints={@UniqueConstraint(columnNames={"memberId", "sessionId"})})
public class ForumMemberOnline implements Serializable{
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long ID;
    private int memberId=0;
    private int uid=0;
    private String sessionId;
    private String nickname="guest";
    //ETC
}

onStart方法保存Session ID

onStop(shiro logout)和onExpiration( session 过期)方法删除在线记录

import net.htage.bbs.web.helper.UserSessionBean;
import net.htage.forum.service.ForumMemberOnlineService;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.SessionListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
/**
 * Shiro session侦听器
 * @author xiaofanku@live.cn
 * @since 20170711
 */
public class BbsUserBeanSessionListener implements SessionListener{
    //Spring 注入
    private String sessionSymbol;
    private static final Logger logger=LoggerFactory.getLogger(BbsUserBeanSessionListener.class);
    @Autowired
    private ForumMemberOnlineService forumMemberOnlineService;

    @Override
    public void onStart(Session session) { //会话创建时触发
        try{
            String sesionId=session.getId().toString();
            logger.error("[UBSL]start id:"+sesionId);
            if(sessionSymbol!=null){
                UserSessionBean usb = new UserSessionBean();
                session.setAttribute(sessionSymbol, usb);
                forumMemberOnlineService.entry(sesionId, usb.getNickname());
            }
        }catch(Exception e){
            logger.error("[UBSL]start exception:"+e.getMessage());
        }
    }

    public void setSessionSymbol(String sessionSymbol) {
        this.sessionSymbol = sessionSymbol;
    }

    @Override
    public void onStop(Session session) {
        try{
            String sesionId=session.getId().toString();
            logger.error("[UBSL]stop for:" + sesionId);
            forumMemberOnlineService.offline(sesionId);
        }catch(Exception e){
            logger.error("[UBSL]stop exception:"+e.getMessage());
        }
    }

    @Override
    public void onExpiration(Session session) {
        try{
            String sesionId=session.getId().toString();
            logger.error("[UBSL]out from:"+sesionId);
            forumMemberOnlineService.offline(sesionId);
        }catch(Exception e){
            logger.error("[UBSL]out exception:"+e.getMessage());
        }
    }

    public void setForumMemberOnlineService(ForumMemberOnlineService forumMemberOnlineService) {
        this.forumMemberOnlineService = forumMemberOnlineService;
    }
}

Spring-shiro.xml配置文件,只列出相关的

    <!-- shiro -->
    <!-- 加载配置属性文件 -->
    <util:properties id="propertyConfigurer" location="classpath:global.properties "/>
    <context:property-placeholder ignore-unresolvable="true" properties-ref="propertyConfigurer" />
    <!-- 自定义session监听器 -->
    <bean id="shiroSessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
        <property name="globalSessionTimeout" value="1800000"/>
        <property name="sessionListeners">
            <list>
                <ref bean="usbSessionListener"/>
            </list>
        </property>
        <property name="sessionValidationInterval" value="3600000"/>
        <property name="sessionValidationSchedulerEnabled" value="true"/>
        <property name="sessionIdCookie" ref="sessionIdCookie"/>
        <property name="sessionIdCookieEnabled" value="true"/>
    </bean>
    <!-- session listener -->
    <bean id="usbSessionListener" class="net.htage.bbs.web.shiro.BbsUserBeanSessionListener">
        <property name="sessionSymbol" value="${site.session.symbol}"/>
    </bean>

3. 当登录时更新线记录 ,将未读的历史消息填充到session bean 的unKnowMsg属性

@Component("memberLoginListener")
public class MemberLoginListener implements ForumEventListener {
    private final static Logger logger = LoggerFactory.getLogger(MemberLoginListener.class);
    @Autowired
    private ForumMemberOnlineService forumMemberOnlineService;
    @Autowired
    private RainMsgService rainMsgService;
    @Value("${site.session.symbol}")
    private String sessionSymbol;

    @Override
    public void handler(ForumEvent event) {
        MemberForumEvent mfe = (MemberForumEvent) event;
        ForumMember member=mfe.getMember();
        GlobalPermissionEnum action=mfe.getAction();
        if(!action.equals(GlobalPermissionEnum.LOGIN)){
            return;
        }
        logger.info("[STAL]login handler start");
        pushSessionBean(member, mfe.getIp());
    }
    //设置
    private void pushSessionBean(ForumMember member, String ipAddr) {
        Subject subject = SecurityUtils.getSubject();
        org.apache.shiro.session.Session shiroSession = subject.getSession();
        UserSessionBean usb = (UserSessionBean) shiroSession.getAttribute(sessionSymbol);

        if (member != null && usb != null) {

            usb.setNickname(member.getNickname());
            usb.setMemberId(member.getId());
            usb.setIp(ipAddr);
            usb.setUserId(member.getUser().getUid());
            //读取历史消息数量
            usb.setUnknowMsg(rainMsgService.getWaitReadMsgSize(member.getUser().getUid()));
            shiroSession.setAttribute(sessionSymbol, usb);
            resetOnlineInfo(shiroSession.getId().toString() ,member);
        }
    }
    //更新在线记录的相关默认值
    private void resetOnlineInfo(String sessionId, ForumMember member){
        forumMemberOnlineService.online(sessionId, member.getNickname(), member.getId(), member.getUser().getUid());
    }
}

4.使用注解标注相应的业务方法,当业务方法执行时发出事件

当有新消息时, 当消息被阅读或删除时, 当登录时. 以RainMsgService为示例说明事件的大概情况

4.1 注解如下

/**
 * @author xiaofanku@live.cn
 * @since 20170607
 */
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MessageActionAnnotation {
    MessageActionEnum action();
}

附注: 如果可以从业务方法的命名中得出可以不使用注解.

4.2 事件对象

/**
 * 基础对象
 * @author xiaofanku@live.cn
 * @since 20170607
 */
public class RainMessageEvent extends EventObject{
    private final int userId;
    //操作类型的枚举
    private final MessageActionEnum action;

    public RainMessageEvent(Object source, int userId, MessageActionEnum action) {
        super(source);
        this.userId=userId;
        this.action=action;
    }

    public int getUserId() {
        return userId;
    }

    public MessageActionEnum getAction() {
        return action;
    }

}

4.3 事件侦听器

/**
 * 
 * @author xiaofanku@live.cn
 * @since 20170607
 */
public interface RainMsgListener extends EventListener{
    /**
     * 适用发件人的操作
     * SEND,READ_FROM,DELE_FROM之一的操作
     * @param event
     * @param messageId 消息ID
     */
    void onMessage(RainMessageEvent event, long messageId);
}

4.4 事件管理器

/**
 * @author xiaofanku@live.cn
 * @since 20170607
 */
public class RainMsgEventManager {
    private static final RainMsgEventManager instance=new RainMsgEventManager();
    private List<RainMsgListener> messageListener;

    private RainMsgEventManager(){
        this.messageListener=new ArrayList<>();
    }

    public static RainMsgEventManager getInstance(){
        return instance;
    }
    //Spring inject start   
    public void setMessageListener(List<RainMsgListener> messageListener) {
        this.messageListener = messageListener;
    }
    //
    public void addMessageListener(RainMsgListener messageListener) {
        this.messageListener.add(messageListener);
    }

    public void forceMessage(int userId, long messageId, MessageActionEnum action){
        if(messageListener==null || messageListener.isEmpty()){
            return;
        }
        notifyMessageListener(new RainMessageEvent(this, userId, action), messageId);
    }

    private void notifyMessageListener(RainMessageEvent rainMessageEvent, long messageId) {
        Iterator<RainMsgListener> it=messageListener.iterator();
        while(it.hasNext()){
            RainMsgListener rml=it.next();
            rml.onMessage(rainMessageEvent, messageId);
        }
    }
}

4.5 Spring AspjectJ

import net.htage.message.entity.RainMsg;
import net.htage.message.event.RainMsgEventManager;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 *
 * @author xiaofanku@live.cn
 * @since 20170607
 */
@Aspect
public class MessageAspect {
    private static final Logger logger=LoggerFactory.getLogger(MessageAspect.class);
    @AfterReturning(
            pointcut="execution(* net.htage.message.service.RainMsgService.*(..)) && @annotation(messageActionAnnotation)", 
            returning="retVal")
    public void action(
            JoinPoint joinPoint, 
            Object retVal, 
            MessageActionAnnotation messageActionAnnotation){
        logger.info("[RMS]aspect start:");
        MessageActionEnum action=messageActionAnnotation.action();
        Object[] args=joinPoint.getArgs();
        if(action.equals(MessageActionEnum.SEND)){
            logger.info("[RMS]aspect send:");
            Long messageId=(Long)retVal;
            if(messageId>0){
                //
                RainMsg rm=(RainMsg)args[0];
                RainMsgEventManager.getInstance().forceMessage(rm.getSender().getUid(), messageId, action);
            }
        }
        if(action.equals(MessageActionEnum.DELE_FROM)){
            logger.info("[RMS]aspect sender delete:");
            Integer userId=(Integer)args[0];
            Long messageId=(Long)args[1];

            RainMsgEventManager.getInstance().forceMessage(userId, messageId, action);

        }
    }
}

4.6 业务接口实现类

    //业务操作
    @MessageActionAnnotation(action=MessageActionEnum.SEND)
    @Override
    public long sendMessage(RainMsg message) {
        RainMsg tmp=rainMsgDao.save(message);
        return tmp.getId();
    }
    @MessageActionAnnotation(action=MessageActionEnum.DELE_FROM)
    @Override
    public boolean deleteMessage(int userId, long messageId) {
        try{
            RainMsg msg=get(messageId);
            msg.setType(RainMsgType.Recycle);
            rainMsgDao.edit(msg);
            return true;
        }catch(Exception e){
            if(logger.isDebugEnabled()){
                logger.debug(e.getMessage(), e);
            }
        }
        return false;
    }

4.7 Spring中的配置

    <!-- 消息侦听器 -->
    <bean class="net.htage.message.event.RainMsgEventManager" factory-method="getInstance">
        <property name="messageListener">
            <list>
                <ref bean="sessionSenderListener"/>
            </list>
        </property>
    </bean>

4.7 侦听器的一个实现

/**
 *
 * @author xiaofanku@live.cn
 * @since 20170831
 */
@Component("sessionSenderListener")
public class SessionSenderListener implements RainMsgListener{
    private static final Logger logger=LoggerFactory.getLogger(SessionSenderListener.class);
    @Value("${site.session.symbol}")
    private String sessionSymbol;
    @Autowired
    private DefaultWebSessionManager shiroSessionManager;
    @Autowired
    private RainMsgService rainMsgService;
    @Autowired
    private ForumMemberOnlineService forumMemberOnlineService;

    @Override
    public void onMessage(RainMessageEvent event, long messageId) {
        if(!event.getAction().equals(MessageActionEnum.SEND)){
            return;
        }
        logger.info("[RMSL][Sender]handler start");
        //A:获得消息
        RainMsg msg=getMessage(messageId);
        if(msg==null){
            logger.info("[RMSL][Sender]msg is null");
            return;
        }
        //B:查找消息接收者的UID
        List<Integer> targets=getMessageRecycles(msg);
        if(targets.isEmpty()){
            logger.info("[RMSL][Sender]target is empty");
            return;
        }
        //C:消息接收者中谁在线
        List<String> sessionIds=getOnlineSessionIds(targets);
        if(sessionIds.isEmpty()){
            logger.info("[RMSL][Sender]no body at line");
            return;
        }
        //D:更新在线的接收者的usb.UnknowMsg + 1
        logger.info("[RMSL][Sender]add UnknowMsg size");
        Iterator<String> it=sessionIds.iterator();
        while(it.hasNext()){
            addRecycleUnknowSize(it.next());
        }
    }
    //自增消息接收者的unknow size
    private void addRecycleUnknowSize(String sessionId){
        try{
            org.apache.shiro.session.Session shiroSession = shiroSessionManager.getSession(new DefaultSessionKey(sessionId));
            UserSessionBean usb = (UserSessionBean) shiroSession.getAttribute(sessionSymbol);
            usb.setUnknowMsg(usb.getUnknowMsg() + 1);
        }catch(Exception e){
            if(logger.isDebugEnabled()){
                logger.debug("[RMSL][Sender]add recycle unknow Size fail", e);
            }
        }
    }
    //A,B,C 没有什么复杂的不贴了
    //ETC
}

最后

  1. 汲及的代码不可能一一在此贴出来,大体思路是这样的仅供参考。截图如下(红色的是推送)
    这里写图片描述

  2. 这里对在线记录的操作可能频繁一些, 如果想提高性能可以使用NoSQL内存数据库来存取.当然对于大型项目本人还是建议使用MQ

  3. maven信息
    <properties>
        <endorsed.dir>${project.build.directory}/endorsed</endorsed.dir>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <!--spring framework-->
        <spring-framework.version>3.2.17.RELEASE</spring-framework.version>
        <!-- spring mvc data to json-->
        <jackson.version>1.9.13</jackson.version>
        <shiro.version>1.3.2</shiro.version>
    </properties>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值