1 问题描述
如题目所示,即实现根据工作时间的设置,创建一个拦截器将用户操作限定在工作时间范围内.
逻辑本身不复杂,但由于项目使用了很多之前没接触过的技术栈,所以写起来有点缺乏信心.好在最后写完了,故将之总结下来.
先列举项目中涉及到的技术栈:
- 前台: 组件vue,vuex,iview,axios
- 后台(分布式): SpringBoot,redis
之后是根据技术栈,列举大致开发步骤:
- 创建拦截器,拦截请求
- 将工作时间的数据放到缓存中
- 逻辑判断:当前时间是否在(缓存中取到的)工作时间内
- (如果不在允许工作时间范围内)跳转至登录页面并提示
2 创建拦截器
由于此前没有创建过拦截器,在做这一步的时候还是很担心能否成功的.
好在这一步比想象中要更轻松,也是多些各位大佬的分享.大致而言,主要分两个步骤:
- 创建拦截器
@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("============================拦截器关闭==============================");
}
}
- 创建拦截器配置
@Configuration
public class WebConfigurer implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
//工作时间拦截器
registry.addInterceptor(workHourInterceptor()).addPathPatterns("/**");
}
@Bean
public WorkHourInterceptor workHourInterceptor(){
return new WorkHourInterceptor();
}
}
对于SpringBoot而言,创建拦截器就是这么简单.
以下参考链接是我在查询相关链接时比较精华的,当然代码和以上是没有太大区别的.
参考链接:
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;
}
}
一二级缓存的名称应该是不需要讲究太多.
参考链接:
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
说明:
- 由于后台是分布式项目,所以对于每一个api项目都可能需要添加拦截器(WorkHourInterceptor)和拦截器(WebConfigurer)配置文件.
- 本文没有对设置缓存数据进行说明,在本项目中有两个对应场景,一是登录时判断有无当前公司的工作时间设置数据,如无则查询并将之放在缓存中,二是修改工作时间设置时,会覆盖本公司当前的工作时间设置缓存数据.