作者 朱兆平

用户心跳续期及在线监测功能

  1 +package com.tianbo.warehouse.controller;
  2 +
  3 +import com.alibaba.fastjson.JSON;
  4 +import com.alibaba.fastjson.JSONObject;
  5 +import com.tianbo.warehouse.controller.response.ResultJson;
  6 +import com.tianbo.warehouse.dao.KakoUserMapper;
  7 +import com.tianbo.warehouse.model.KakoUser;
  8 +import com.tianbo.warehouse.util.RedisUtils;
  9 +import io.swagger.annotations.ApiOperation;
  10 +import lombok.extern.slf4j.Slf4j;
  11 +import org.apache.commons.lang.StringUtils;
  12 +import org.springframework.beans.factory.annotation.Autowired;
  13 +import org.springframework.web.bind.annotation.PostMapping;
  14 +import org.springframework.web.bind.annotation.RestController;
  15 +
  16 +import javax.annotation.Resource;
  17 +import javax.servlet.http.HttpServletRequest;
  18 +import javax.servlet.http.HttpServletResponse;
  19 +import java.util.Date;
  20 +
  21 +@RestController()
  22 +@Slf4j
  23 +public class HeartBeatController {
  24 +
  25 + @Autowired
  26 + private RedisUtils redisUtils;
  27 +
  28 + @Resource
  29 + private KakoUserMapper kakoUserMapper;
  30 +
  31 + //token头部标识类型,Bearer代表Bearer TOKEN
  32 + static final String AUTHORIZATION_HEADER = "Bearer ";
  33 +
  34 + //检查token时效是否低于标准线
  35 + static final long TOKEN_TTL_CHECK_MIN= 36000L;
  36 +
  37 + //重置token时效为标准线
  38 + static final long TOKEN_TTL_ADD= 86400L;
  39 +
  40 + //心跳每次续费时长
  41 + static final long HEARTBEAT_TOKEN_TTL_ADD= 10L;
  42 +
  43 + @ApiOperation(value = "用户心跳接口", notes = "心跳续期")
  44 + @PostMapping("/heartbeat")
  45 + public ResultJson heartbeat(HttpServletRequest request, HttpServletResponse response){
  46 + try {
  47 +
  48 + //1. 获取客户端IP,因为有反向代理所以要从头部获取代理过来的头部IP
  49 + String clientIP =null;
  50 + clientIP = request.getRemoteAddr();
  51 + String header_forwarded = request.getHeader("x-forwarded-for");
  52 + if (StringUtils.isNotBlank(header_forwarded)) {
  53 + clientIP = request.getHeader("x-forwarded-for");
  54 + // 多次反向代理后会有多个ip值,第一个ip才是真实ip
  55 + if (clientIP.contains(",")) {
  56 + clientIP = clientIP.split(",")[0];
  57 + }
  58 + }
  59 +
  60 + //2.获取token
  61 + String token = request.getHeader("Authorization");
  62 + /**
  63 + * key样式
  64 + * accessToken:token
  65 + */
  66 + if (token!=null && !token.isEmpty() && token.startsWith(AUTHORIZATION_HEADER)){
  67 + token = token.substring(AUTHORIZATION_HEADER.length());
  68 + String accessToken = token;
  69 + String userDetailStr = redisUtils.get(accessToken);
  70 +
  71 +
  72 + //4. 更新用户心跳时间及在线状态IP等资料
  73 + if (StringUtils.isNotBlank(userDetailStr)){
  74 +
  75 + JSONObject u = JSON.parseObject(userDetailStr);
  76 + String userId= u.getString("id");
  77 + String userInfo = u.getString("name");
  78 + String username = u.getString("username");
  79 +// userDetailStr = userDetailStr.replace("@","");
  80 +
  81 + /**3.续期token过期时间
  82 + * 增加过期时间,考虑到程序及网络传输中间的时间损耗,
  83 + * 每10秒一个的心跳直接续费10秒的话,token的过期时间还是会随着时间逐步减少
  84 + */
  85 + long tokenExpireTime= redisUtils.getExpire(accessToken);
  86 + if(tokenExpireTime < TOKEN_TTL_CHECK_MIN){
  87 + redisUtils.expire(accessToken, TOKEN_TTL_ADD);
  88 + redisUtils.expire(username, TOKEN_TTL_ADD);
  89 + }else{
  90 + redisUtils.expire(accessToken,tokenExpireTime+HEARTBEAT_TOKEN_TTL_ADD);
  91 + redisUtils.expire(username, tokenExpireTime+HEARTBEAT_TOKEN_TTL_ADD);
  92 + }
  93 +
  94 + /**
  95 + * 多式联运用户表
  96 + */
  97 +// Integer dsly_userId = u.getInteger("id");
  98 +// USER user = new USER();
  99 +// user.setId(dsly_userId);
  100 +// user.setLoginip(clientIP);
  101 +// user.setLogintime(new Date());
  102 +// user.setOnline(true);
  103 +// int ii = userMapper.updateByPrimaryKeySelective(user);
  104 +
  105 + KakoUser kakoUser = new KakoUser();
  106 + kakoUser.setId(userId);
  107 + kakoUser.setLoginIp(clientIP);
  108 + kakoUser.setLoginDate(new Date());
  109 + kakoUser.setOnline(true);
  110 + int i = kakoUserMapper.updateByPrimaryKeySelective(kakoUser);
  111 +
  112 + return i > 0 ? new ResultJson("200","心跳成功"): new ResultJson("400","心跳失败");
  113 + }
  114 +
  115 + }
  116 + return new ResultJson("400","心跳失败");
  117 +
  118 + }catch (Exception e){
  119 + log.error("[HEART-BEAT-ERROR]-",e);
  120 + return new ResultJson("400","心跳失败");
  121 + }
  122 + }
  123 +}
  1 +package com.tianbo.warehouse.heatbeat;
  2 +
  3 +
  4 +import com.tianbo.warehouse.dao.KakoUserMapper;
  5 +import lombok.extern.slf4j.Slf4j;
  6 +import org.springframework.stereotype.Component;
  7 +import com.tianbo.warehouse.model.KakoUser;
  8 +
  9 +import javax.annotation.PostConstruct;
  10 +import javax.annotation.Resource;
  11 +import java.util.Date;
  12 +
  13 +@Component
  14 +@Slf4j
  15 +public class OfflineTheardJob implements Runnable {
  16 +
  17 + private static OfflineTheardJob offlineTheardJob;
  18 +
  19 + private KakoUser user;
  20 +
  21 + //用户掉线判定时间差
  22 + static final long OFFLINE_= 60L;
  23 +
  24 + @Resource
  25 + private KakoUserMapper userMapper;
  26 +
  27 + OfflineTheardJob() {
  28 +
  29 + }
  30 + OfflineTheardJob(KakoUser user) {
  31 + this.user = user;
  32 + }
  33 +
  34 + @PostConstruct
  35 + public void init(){
  36 + offlineTheardJob = this;
  37 + }
  38 +
  39 + @Override
  40 + public void run(){
  41 + Date userLoginTime = user.getLoginDate();
  42 + if(userLoginTime!=null){
  43 + long diff= Math.abs(System.currentTimeMillis() - userLoginTime.getTime());
  44 + long s = diff / 1000;
  45 +
  46 + log.info("[HEAT-BEAT]-用户{}心跳-时间相差{}秒",user.getName(),s);
  47 +
  48 +
  49 + if (s > OFFLINE_){
  50 + setOffline();
  51 + }
  52 + }else {
  53 + setOffline();
  54 + }
  55 + }
  56 +
  57 + private void setOffline(){
  58 + user.setOnline(false);
  59 + int i = offlineTheardJob.userMapper.updateByPrimaryKeySelective(user);
  60 + if (i>0){
  61 + log.info("用户id:{},用户名称:{},从IP:{}掉线",user.getId(),user.getName(),user.getLoginIp());
  62 + }
  63 + }
  64 +}
  1 +package com.tianbo.warehouse.heatbeat;
  2 +
  3 +import com.tianbo.warehouse.dao.KakoUserMapper;
  4 +import com.tianbo.warehouse.model.KakoUser;
  5 +import lombok.extern.slf4j.Slf4j;
  6 +import org.springframework.scheduling.annotation.Scheduled;
  7 +import org.springframework.stereotype.Component;
  8 +
  9 +import javax.annotation.Resource;
  10 +import java.util.List;
  11 +import java.util.concurrent.ThreadPoolExecutor;
  12 +
  13 +
  14 +/**
  15 + * 清理心跳超时的在线用户,判定为离线
  16 + * @author xyh
  17 + * @date
  18 + * 记得给用户ID,用户名称,用户心跳时间,用户登录ip,用户在线状态的数据库字段设置索引。
  19 + */
  20 +@Slf4j
  21 +@Component
  22 +public class OfflineUserTask {
  23 +
  24 +
  25 + @Resource
  26 + private KakoUserMapper userMapper;
  27 +
  28 + @Scheduled(fixedRate = 60000)
  29 + private void offlineUserHeartBeat(){
  30 +
  31 + //初始化线程池
  32 + ThreadPoolExecutor threadPool = XMLThreadPoolFactory.instance();
  33 +
  34 + List<KakoUser> userList = userMapper.selectOnlineUser();
  35 + if (userList!=null && !userList.isEmpty()){
  36 + log.trace("用户掉线判定开始,共需判定{}个在线标识用户",userList.size());
  37 + for (KakoUser user:userList) {
  38 + OfflineTheardJob offlineTheardJob = new OfflineTheardJob(user);
  39 + threadPool.execute(offlineTheardJob);
  40 + }
  41 +
  42 + }
  43 +
  44 + }
  45 +}
  46 +
  1 +package com.tianbo.warehouse.heatbeat;
  2 +
  3 +import java.util.ArrayList;
  4 +import java.util.Date;
  5 +import java.util.Iterator;
  6 +import java.util.List;
  7 +import java.util.concurrent.ThreadFactory;
  8 +
  9 +public class XMLThreadFactory implements ThreadFactory {
  10 +
  11 + private int counter;
  12 + private String name;
  13 + private List<String> stats;
  14 +
  15 + public XMLThreadFactory(String name)
  16 + {
  17 + counter = 1;
  18 + this.name = name;
  19 + stats = new ArrayList<String>();
  20 + }
  21 +
  22 + @Override
  23 + public Thread newThread(Runnable runnable)
  24 + {
  25 + Thread t = new Thread(runnable, name + "-Thread_" + counter);
  26 + counter++;
  27 + stats.add(String.format("Created thread %d with name %s on %s \n", t.getId(), t.getName(), new Date()));
  28 + return t;
  29 + }
  30 +
  31 + public String getStats()
  32 + {
  33 + StringBuffer buffer = new StringBuffer();
  34 + Iterator<String> it = stats.iterator();
  35 + while (it.hasNext())
  36 + {
  37 + buffer.append(it.next());
  38 + }
  39 + return buffer.toString();
  40 + }
  41 +
  42 +}
  1 +package com.tianbo.warehouse.heatbeat;
  2 +
  3 +import java.util.concurrent.LinkedBlockingQueue;
  4 +import java.util.concurrent.ThreadPoolExecutor;
  5 +import java.util.concurrent.TimeUnit;
  6 +
  7 +public class XMLThreadPoolFactory {
  8 +
  9 + private static ThreadPoolExecutor threadPool;
  10 +
  11 + public static ThreadPoolExecutor instance(){
  12 + if (threadPool==null){
  13 + XMLThreadFactory xmlThreadFactory = new XMLThreadFactory("heartbeatTask");
  14 + threadPool = new ThreadPoolExecutor(12, 64,
  15 + 0L, TimeUnit.MILLISECONDS,
  16 + new LinkedBlockingQueue<Runnable>(1024),
  17 + xmlThreadFactory,
  18 + new ThreadPoolExecutor.AbortPolicy());
  19 + }
  20 + return threadPool;
  21 + }
  22 +}