分布式项目:根据工作时间设置对请求拦截并跳转

原创
2019/06/27 17:49
阅读数 353

1 问题描述

如题目所示,即实现根据工作时间的设置,创建一个拦截器将用户操作限定在工作时间范围内.
逻辑本身不复杂,但由于项目使用了很多之前没接触过的技术栈,所以写起来有点缺乏信心.好在最后写完了,故将之总结下来.
先列举项目中涉及到的技术栈:

  • 前台: 组件vue,vuex,iview,axios
  • 后台(分布式): SpringBoot,redis

之后是根据技术栈,列举大致开发步骤:

  1. 创建拦截器,拦截请求
  2. 将工作时间的数据放到缓存中
  3. 逻辑判断:当前时间是否在(缓存中取到的)工作时间内
  4. (如果不在允许工作时间范围内)跳转至登录页面并提示

2 创建拦截器

由于此前没有创建过拦截器,在做这一步的时候还是很担心能否成功的.
好在这一步比想象中要更轻松,也是多些各位大佬的分享.大致而言,主要分两个步骤:

  1. 创建拦截器
@Component
public class WorkHourInterceptor implements HandlerInterceptor {
    private static Logger logger= LoggerFactory.getLogger(WorkHourInterceptor.class);

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 如果不是映射到方法直接通过
        if(!(handler instanceof HandlerMethod)){
            return true;
        }
        logger.info("============================拦截器启动==============================");
        request.setAttribute("starttime",System.currentTimeMillis());
        
        //TODO 这里是逻辑代码,放在逻辑判断中说

        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        logger.info("===========================执行处理完毕=============================");
        long starttime = (long) request.getAttribute("starttime");
        request.removeAttribute("starttime");
        long endtime = System.currentTimeMillis();
        logger.info("============请求地址:/cf"+request.getRequestURI()+":处理时间:{}",(endtime-starttime)+"ms");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        logger.info("============================拦截器关闭==============================");
    }
}
  1. 创建拦截器配置
@Configuration
public class WebConfigurer implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //工作时间拦截器
        registry.addInterceptor(workHourInterceptor()).addPathPatterns("/**");
    }

    @Bean
    public WorkHourInterceptor workHourInterceptor(){
        return new WorkHourInterceptor();
    }
}

对于SpringBoot而言,创建拦截器就是这么简单.
以下参考链接是我在查询相关链接时比较精华的,当然代码和以上是没有太大区别的.

参考链接:

  1. Spring Boot 优雅的配置拦截器方式
  2. SpringBoot 2.X配置登录拦截器
  3. Spring Boot 拦截器
  4. Spring Boot配置拦截器

3 将工作时间数据放到缓存中

对于redis在java中的使用,项目中使用的是二级缓存j2cache.
其具体的原理本文不再详述,这里只谈使用.(由于该代码与逻辑代码是放在一个类中的,故不再分开.其中可能涉及到一些在其他项目中没有必要追加的计算,会简要说明)

public class WorkHourUtil {

    private static final String WORK_HOUR="WorkHour";

    private static final String SERVICE_NAME="CfApi";

    private static final String COMMON="Common";

    public static boolean isWorkHourEmpty(String strCompanyId){
        //工作时间缓存数据是否为空
        return getCacheData(strCompanyId)==null;
    }

    /**
     * Is in work hour boolean.
     *
     * @param strCompanyId the str company id
     * @return the boolean
     * @author YangYishe
     * @date 2019年06月27日 16:08
     */
    static boolean isInWorkHour(String strCompanyId){
        //是否在工作时间内(含获取数据过程)
        List<WorkingHours> lstWh=getCacheData(strCompanyId);
        return isInWorkHour(lstWh);
    }

    private static boolean isInWorkHour(List<WorkingHours> lstWorkHour){
        //是否在工作时间内(仅需逻辑有关)
        //此处的代码在下面的逻辑判断中有用到,不再额外贴出
        if(lstWorkHour==null||lstWorkHour.size()==0){
            return true;
        }
        Date mToday=new Date();
        //此处获取到的星期是把周日当第1天算的
        //hutool的DateUtil
        int intWeekDay= DateUtil.dayOfWeek(mToday)-1;
        if(intWeekDay<=0){
            intWeekDay+=7;
        }
        DateTime mNow=DateUtil.parseTime(DateUtil.formatTime(mToday));
        for(WorkingHours mWh:lstWorkHour){
            if(mWh.getSort().equals(intWeekDay)){
                boolean blnIsEnable=mWh.getDayState();
                DateTime mStartTime=DateUtil.parseTime(mWh.getStartTime());
                DateTime mEndTime=DateUtil.parseTime(mWh.getEndTime());
                boolean blnAfterStart=mNow.isAfterOrEquals(mStartTime);
                boolean blnBeforeEnd=mNow.isBeforeOrEquals(mEndTime);
                return (!blnIsEnable)||(blnAfterStart&&blnBeforeEnd);
            }
        }
        //此处一般不会触发,如果触发了,就需要检查代码看哪儿有问题了.
        return false;
    }

    private static String getRegion(String strCompanyId){
        //由于项目是分企业的,每个企业都有自己独自的工作时间缓存数据
        return WORK_HOUR+"∥"+strCompanyId;
    }

    public static void setCacheData(String strCompanyId,List<WorkingHours> lstWh){
        CacheChannel channel = J2Cache.getChannel();
        String cacheKey = StrUtil.format("{}{}",SERVICE_NAME, COMMON);
        String strRegion=getRegion(strCompanyId);
        //不确定缓存能否保存WorkHours的结合,以防万一,这里直接转换成了字符串
        //这里的JSON是FastJson,我目前用过最方便的JSON解析包
        String strWhList=JSON.toJSONString(lstWh);
        channel.set(strRegion,cacheKey,strWhList);
    }

    private static List<WorkingHours> getCacheData(String strCompanyId){
        List<WorkingHours> lstWh=null;
        CacheChannel channel = J2Cache.getChannel();
        //这里的次级索引合并在其他项目中作用也不是很大,可只取一个字符串
        String cacheKey = StrUtil.format("{}{}",SERVICE_NAME, COMMON);
        Object object=channel.get(getRegion(strCompanyId),cacheKey).getValue();
        if(ObjectUtil.isNotNull(object)){
            String strWhList= (String) object;
            lstWh=JSON.parseArray(strWhList,WorkingHours.class);
        }
        return lstWh;
    }
}

一二级缓存的名称应该是不需要讲究太多.

参考链接:

  1. J2Cache官方API
  2. java项目集成J2Cache

4 逻辑判断

这里首先要参考的是,获取当前时间是否在工作时间内,直接调用WorkHourUtil的isInWorkHour方法即可.
该方法要在拦截器中拦截.代码大致如下(以下方法写在WorkHourInterceptor类preHandle方法的TODO处):

List<String> lstUrlLogin= Arrays.asList("/login","/logout");
//这里用lambda表达式判断当前地址是否为登录或登出地址
boolean blnIsLogin=lstUrlLogin.stream().anyMatch(m->m.equals(request.getRequestURI()));
if(!blnIsLogin){
    //获取企业id(这里的getCompanyId是获取公司id的方法,其他项目可以不关注,或者有这里的获取逻辑)
    String strCompanyId=getCompanyId(request);
    //此处判断是否在工作时间
    boolean blnIsInWorkHour=WorkHourUtil.isInWorkHour(strCompanyId);
    if(!blnIsInWorkHour){
        Map<String,Object> mapResult=new HashMap<>();
        mapResult.put("logout",true);
        mapResult.put("message","当前时间不允许使用系统!");
        //注:returnJson是判断后的页面跳转,放在第5大点说
        returnJson(response,JSON.toJSONString(mapResult));
        return false;
    }
}

5 判断后的页面跳转

这里先贴上后台的returnJson方法(同样在WorkHourInterceptor中),用以表示请求被拦截:

private void returnJson(HttpServletResponse response, String json) throws Exception {
    response.setCharacterEncoding("UTF-8");
    response.setContentType("text/html; charset=utf-8");
    try (PrintWriter writer = response.getWriter()) {
        writer.print(json);
    } catch (IOException e) {
        logger.error("response error", e);
    }
}

不同于以前开发的页面和后台放在一起的项目,当前项目只有后台,发送回的请求也全部都是JSON数据,换言之,并不关联页面.
所以,想要判断后进行页面跳转,必须在前台项目中的拦截器中拦截该请求再进行跳转.(这里前台页面拦截器的代码很长,但相当多的部分与本文所述的要求并无直接关系,同时不方便删除,故保留,阅读时请注意甄别)
登录拦截js(需在axios.js中调用)

import store from '../../store/index.js'
import router from '../../router/index.js'
import { isEmpty } from '../../view/components/about-text/about-string'
import { Message } from 'iview'

/**
 * 登出拦截器(结果是登出的拦截器)
 * @param res
 */
export const logoutInterceptor = (res) => {
  // 判断,当返回结果中含有返回值提示应当跳转到logout页面时,则跳转logout页面
  if (typeof res.data !== 'undefined') {
    if (res.data.logout) {
    //这里的handleLogOut是发送登出请求的mapAction中的方法,用以去除一些session和token数据
      store.dispatch('handleLogOut').then(() => {
        router.push({
          name: 'login'
        })
        //登录页面的同时提示是由于何原因登出
        if (!isEmpty(res.data.message)) {
          Message.warning(res.data.message)
        }
      })
    }
  }
}

前台拦截器(axios),注意loginInterceptor仅在相应拦截中调用了,其他地方均与本文要说的内容无关,只是为了避免个别人摸不清头脑所以把全部代码都写上了:

import axios from 'axios'
import { getToken } from '@/libs/util'
import { isEmpty } from '../view/components/about-text/about-string'
import { logoutInterceptor } from '../api/system/login'

let arrRequestUrl = {}
const CancelToken = axios.CancelToken

class HttpRequest {
  constructor (baseUrl = baseURL) {
    this.baseUrl = baseUrl
    this.queue = {}
  }
  getInsideConfig () {
    const config = {
      baseURL: this.baseUrl,
      headers: {
        Authorization: getToken()
      }
    }
    return config
  }
  destroy (url) {
    delete this.queue[url]
    if (!Object.keys(this.queue).length) {
      // Spin.hide()
    }
  }
  interceptors (instance, url) {
    // 请求拦截
    instance.interceptors.request.use(config => {
      // 发起请求时,如果正在发送的请求中,已有对应的url,则驳回,否则记录
      // 此处的get方法,不加拦截也ok,加之领导要求,于是改为get方法不加拦截.
      if (arrRequestUrl[url] && config.method !== 'get') {
        return Promise.reject(new Error('repeatSubmit'))
      } else {
        arrRequestUrl[url] = true
      }
      // 添加全局的loading...
      if (!Object.keys(this.queue).length) {
        // Spin.show() // 不建议开启,因为界面不友好
      }
      this.queue[url] = true
      return config
    }, error => {
      return Promise.reject(error)
    })
    // 响应拦截
    instance.interceptors.response.use(res => {
      // 去掉正在发送请求的记录
      delete arrRequestUrl[url]
      // 结果是登出的拦截器!!!
      logoutInterceptor(res)

      this.destroy(url)
      const { data, status } = res
      return { data, status }
    }, error => {
      // 对重复提交的错误信息进行解析
      if (error.message === 'repeatSubmit') {
        throw new Error('请不要重复提交')
      } else {
        delete arrRequestUrl[url]
      }
      this.destroy(url)
      let errorInfo = error.response
      if (!errorInfo) {
        const { request: { statusText, status }, config } = JSON.parse(JSON.stringify(error))
        errorInfo = {
          statusText,
          status,
          request: { responseURL: config.url }
        }
      }
      // addErrorLog(errorInfo)
      return Promise.reject(error)
    })
  }
  request (options) {
    const instance = axios.create()
    options = Object.assign(this.getInsideConfig(), options)
    this.interceptors(instance, options.url)
    return instance(options)
  }
}
export default HttpRequest

说明:

  1. 由于后台是分布式项目,所以对于每一个api项目都可能需要添加拦截器(WorkHourInterceptor)和拦截器(WebConfigurer)配置文件.
  2. 本文没有对设置缓存数据进行说明,在本项目中有两个对应场景,一是登录时判断有无当前公司的工作时间设置数据,如无则查询并将之放在缓存中,二是修改工作时间设置时,会覆盖本公司当前的工作时间设置缓存数据.
展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部