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
}
最后
汲及的代码不可能一一在此贴出来,大体思路是这样的仅供参考。截图如下(红色的是推送)
这里对在线记录的操作可能频繁一些, 如果想提高性能可以使用NoSQL内存数据库来存取.当然对于大型项目本人还是建议使用MQ
- 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>