欢迎光临
我们一直在努力

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


1 问题描述

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

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

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

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

2 创建拦截器

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

  1. 创建拦截器
@Componentpublic 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. 创建拦截器配置
@Configurationpublic 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.CancelTokenclass 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)
未经允许不得转载:ITyet » 分布式项目:根据工作时间设置对请求拦截并跳转
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址