笔者最近喜欢用“笔者”二字称呼自己,说来也搞笑,大三以来到如今大四实习等待转正学习编程之前,笔者本人其实是文学社的成员,创作诗词与短篇小说。没想到喜欢上了编程。闲话少叙,各位见谅。
平台介绍
面向什么行业,能做什么,能为用户带来什么?
面向当今社会奋斗狂潮(其实就是绝大部分人离开农村,去往城市打工,老人留守过多)下的留守老人入住敬老院的服务,可以使老人的子女能看到敬老院的实况,老人的健康情况,老人的居住实况,敬老院的医养计划情况,护工护理工作情况。家人放心,老人舒心,可以为企业分析流水,信息管理,订单回溯,实时通过平台与老人家人联系,整合信息,修改策略,敬老院也需要互联网赋能,敬老院也需要互联网+。
平台架构:
后端架构springBoot框架,采用redis与ThreadLocal共存信息权限校验,采用线程池定时任务触发,采用智能excel表格导入与导出,采用自定义请求头信息校验身份。前端采用vue2与elementUI设计模板,结合router,axios,site等常用vue技术。
架构优势:
代码极其简洁,可读性极高,耦合度低,高度抽离公用工具,拥有独特的上下文设计,线程隔离级别用户信息保护。
平台局部页面预览
客户服务中心局部预览
后台管理中心局部预览
平台架构关键技术实现干货
笔者是java全干,编写这套系统的初衷是为了一个需求,由于非笔者自己公司的工作,笔者采用的技术点广泛,代码简洁优雅,缩减业务代码的重复度,减少查库是架构最大的特性,当然也包括线程隔离级用户信息保护。
ThreadLocal的使用
本地线程也可以当做局部线程,这个变量拿到了一个引用,真实的数据存储在堆中,每一个线程拿到的都是新的,内容也都是需要重新注入的,这也就保证了线程间的数据不会互相暴露,但是每一次每一个线程运用到它的时候,都会拿到一个新的引用,那么引用也是会占据jvm内存空间的,所以引用的删除也是业务执行之后必不可少的一部分(且需要注意的是如果在请求结束后视图渲染完处理的话,如果执行过程抛出异常,又不处理响应,接口超时,引用就不会remove掉)
通过它的这些特性,我们就可以实现redis与它的存储用户信息,权限等关键公用字段,当然redis不安全我们可以仅仅把不是很重要的信息存进redis比如账户,姓名等。
笔者在平台中对ThreadLocal使用的实现代码
@Slf4j
public class AppContext {
/*transient 保护该上下文安全 限定内存存储 不允许被传输到磁盘或者被进行网络传输*/
static transient ThreadLocal<Map<String, String>> contextMap = new ThreadLocal<>();
/*项目单体没有微服务架构概念 app_code作为微服务标识 所以可以给定值*/
public final static String APP_CODE = "live";
/*当前用户类型 employee管理员账号 与 elderSon用户账号*/
public final static String USER_TYPE = "user_type";
/*当前用管理员用户权限等级 admin是普通管理员权限为 1 */
public final static String USER_ROLE_ADMIN = "user_role_admin";
/*当前管理员用户权限等级 superAdmin是超级管理员权限为 2以上*/
public final static String USER_ROLE_SUPER_ADMIN = "user_role_super_admin";
/*当前用户id 对应数据库主键id值*/
public final static String USER_ID = "user_id";
/*当前用户姓名 对应数据库name字段值 不为空*/
public final static String USER_NAME = "user_name";
/*当前用户电话 对应数据库phone字段值 可为空*/
public final static String USER_PHONE = "user_phone";
/*当前管理员用户权限等级 对应数据库auth_level字段值 用于判断是否为管理员 以及管理员类型*/
public final static String USER_AUTH_LEVEL = "user_auth_level";
/*当前管理员用户职位 对应数据库unit字段值*/
public final static String USER_UNIT = "user_unit";
/*钉钉登录微应用信息*/
public final static String APP_LOGIN_DING_ID = "app_login_ding_id";
public final static String APP_LOGIN_DING_SECRET = "app_login_ding_secret";
public final static String APP_LOGIN_RETURN_URL = "app_login_return_url";
/*融云聊天平台应用信息*/
public final static String CLOUD_CHAT_KEY = "cloud_chat_key";
public final static String CLOUD_CHAT_SECRET = "cloud_chat_secret";
public final static String CLOUD_NET_URI = "cloud_net_uri";
public final static String CLOUD_NET_URI_RESERVE = "cloud_net_uri_reserve";
public static Map<String, String> getContextMap() {
return contextMap.get();
}
public static Map<String, String> getNewContextMap() {
Map<String, String> map = contextMap.get();
return map == null ? new HashMap<>() : new HashMap<>(map);
}
public static void setContextMap(Map<String, String> context) {
if (context == null) {
contextMap.remove();
} else {
contextMap.set(context);
}
}
public static String get(String key) {
Map<String, String> map = getContextMap();
return map == null ? null : map.get(key);
}
public static String put(String key, String value) {
Map<String, String> map = getContextMap();
if (map == null) {
map = new HashMap<>();
setContextMap(map);
}
return map.put(key, value);
}
public static void setDingVerifyInfo(DingAppInfo dingAppInfo) {
if (dingAppInfo.checkKeyWordIsNotNull(dingAppInfo)) {
put(APP_LOGIN_DING_ID, dingAppInfo.getApp_id());
put(APP_LOGIN_DING_SECRET, dingAppInfo.getApp_secret());
put(APP_LOGIN_RETURN_URL, dingAppInfo.getApp_return_url());
}
}
public static void setCloudChatInfo(CloudChatAppInfo cloudChatAppInfo) {
if (cloudChatAppInfo.checkKeyWordIsNotNull(cloudChatAppInfo)) {
put(CLOUD_CHAT_KEY, cloudChatAppInfo.getCloud_key());
put(CLOUD_CHAT_SECRET, cloudChatAppInfo.getCloud_secret());
put(CLOUD_NET_URI, cloudChatAppInfo.getCloud_net_uri());
put(CLOUD_NET_URI_RESERVE, cloudChatAppInfo.getCloud_net_uri_reserve());
}
}
public static void setContextMapData(Context context) {
put(USER_TYPE, context.getUser_type());
put(USER_ID, context.getUser_id());
if (StringUtils.hasText(context.getUser_phone())) {
put(USER_PHONE, context.getUser_phone());
}
put(USER_NAME, context.getUser_name());
if (Objects.equals(AppContext.get(USER_TYPE), "employee")) {
put(USER_AUTH_LEVEL, context.getUser_auth_level());
if (Integer.parseInt(AppContext.getUserAuthLevel()) >= 2) {
put(USER_ROLE_SUPER_ADMIN, "true");
log.info(context.getUser_name() + "认证管理员成功");
} else {
log.info(context.getUser_name() + "认证职工成功");
put(USER_ROLE_ADMIN, "true");
}
put(USER_UNIT, context.getUser_unit());
}
}
public static String getUserId() {
return get(USER_ID);
}
public static String getUserPhone() {
return get(USER_PHONE);
}
public static String getUserName() {
return get(USER_NAME);
}
public static String getUserAuthLevel() {
return get(USER_AUTH_LEVEL);
}
public static String getUserUnit() {
return get(USER_UNIT);
}
public static String getAppLoginDingId() {
return get(APP_LOGIN_DING_ID);
}
public static String getAppLoginDingSecret() {
return get(APP_LOGIN_DING_SECRET);
}
public static String getAppLoginReturnUrl() {
return get(APP_LOGIN_RETURN_URL);
}
public static String getCloudChatKey() {
return get(CLOUD_CHAT_KEY);
}
public static String getCloudChatSecret() {
return get(CLOUD_CHAT_SECRET);
}
public static String getCloudNetUri() {
return get(CLOUD_NET_URI);
}
public static String getCloudNetUriReserve() {
return get(CLOUD_NET_URI_RESERVE);
}
public static String getAppCode() {
return APP_CODE;
}
public static String getUserType() {
return get(USER_TYPE);
}
public static boolean getUserRoleAdmin() {
return Objects.equals(get(USER_ROLE_ADMIN), "true");
}
public static boolean getUserRoleSuperAdmin() {
return Objects.equals(get(USER_ROLE_SUPER_ADMIN), "true");
}
public static boolean isAdmin() {
return getUserRoleAdmin() || getUserRoleSuperAdmin();
}
public static boolean isSuperAdmin() {
return getUserRoleSuperAdmin();
}
public static boolean checkLoginContext() {
return StringUtils.hasText(AppContext.getAppLoginDingId())
&& StringUtils.hasText(AppContext.getCloudChatKey());
}
public static boolean checkContextUserInfo() {
return StringUtils.hasText(AppContext.getUserType())
&& StringUtils.hasText(AppContext.getUserId())
&& StringUtils.hasText(AppContext.getUserName());
}
public static void fillLoginContext() {
DingAppInfo appInfo = SpringContextHolder.getBean(DingAppInfoService.class).findAppInfo(APP_CODE);
setDingVerifyInfo(appInfo);
CloudChatAppInfo cloudChatAppInfo = SpringContextHolder.getBean(CloudChatAppInfoService.class).findAppInfo(APP_CODE);
setCloudChatInfo(cloudChatAppInfo);
}
public static void clear() {
contextMap.remove();
}
}
笔者不再为大家分析代码中的某些实现逻辑(例如怎么在静态方法里还调用了组件的查库方法,如果您看过笔者的前几篇文章,您就会发现笔者就是实际问题实际分析得来的总结干货)
平台中能使用到ThreadLocal的业务场景
首先是请求的拦截器中使用,拦截器前端控制器方法(也就是开发人员口中的api)执行前需要获取令牌信息与类型信息,校验通过后填充本线程用户数据:
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
try {
if (HttpMethod.OPTIONS.toString().equals(request.getMethod())) {
return true;
}
String token = request.getHeader("token");
String user_type = request.getHeader("user_type");
if (StringUtils.hasText(token) && StringUtils.hasText(user_type)) {
Context context = new Context();
if (Objects.equals(user_type, "elder_son")) {
ElderSon elderSon = elderSonService.getElderSonByElderSonId(token);
context.setContextByElderSon(elderSon);
return true;
} else if (Objects.equals(user_type, "employee")) {
Employee employee = employeeService.getEmployeeById(token);
context.setContextByEmployee(employee);
return true;
}
} else if (StringUtils.hasText(user_type)) {
return false;
}
return false;
} catch (Exception e) {
AppContext.clear();
return false;
} finally {
System.gc();
}
}
从上述代码中我们不难看到对于预检行为的放行,这是因为笔者简单的自定义了请求头,并没有对cookie做处理,自定义的请求头除了我们跨域问题需要处理,还需要处理的就是预检行为。
之前笔者说过remove这次引用极其重要
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
AppContext.clear();
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
具体效果就是对于某些业务方法需要校验权限才能允许执行,但是这里对于员工的权限校验一般的开发人员会直接查库,查redis,但是本地线程的速度很快且可以为redis分担压力(这里举例需要校验权限的查询全部老人信息方法)
public List<Elder> findElderList() {
if (!AppContext.isAdmin()) {
return null;
}
List<Elder> list = list();
return Objects.nonNull(list) ? list.stream().sorted(Comparator.comparing(Elder::getElder_age)).collect(Collectors.toList()) : null;
}
无需查看,无需查redis,只需获取调用静态方法获取当前线程数据即可
定时任务工具
实现定时任务的方案有很多,目前笔者自己常用的有:
-
xxl-job
-
springboot 的 @Scheduled
-
Quartz 框架
但是这不代表就这些:
-
使用java线程实现
-
使用java的TimerTask
-
redis键值过期监听回调
注意最后三种不适合真实业务场景,时间误差大,四五笔者就不解释了,六的问题属于机制问题,惰性删除机制,不明白的读者可以去查阅百度。
笔者给朋友写的,涉及定时任务的业务对于准确性要求不高,故而笔者这里采取全局线程池放上线程去实现定时,也就是第四种(注意笔者进行了初次封装与实验,但不保证高并发会稳定)。
笔者自封装TaskExe定时任务下发工具实现代码
public class TaskExe {
private static final TimerHashMap cache = TimerCacheFactory.getCache();
private static ThreadPoolTaskExecutor executor = null;
private static final ThreadPoolTaskExecutor USER_THREAD_POOL_TASK_EXECUTOR = getUserThreadPoolTaskExecutor();
public TaskExe() {
}
public static void setTimer(String serverKey, TaskInfo taskInfo) {
boolean sign = cache.containsKey(serverKey);
if (!sign) {
try {
USER_THREAD_POOL_TASK_EXECUTOR.submit(() -> {
TaskInfo put = cache.put(serverKey, taskInfo);
if (put == null) {
try {
Thread.sleep((long)(taskInfo.getTaskTime() * 1000));
if (taskInfo.getType() == TaskType.TASK_DELETE_CACHE) {
TaskInfo remove = cache.remove(serverKey);
if (remove == null) {
throw new Exception("计时任务删除失败");
}
} else if (taskInfo.getType() == TaskType.TASK_INTER_CACHE) {
boolean result = cache.endTaskInCache(serverKey, taskInfo);
if (!result) {
throw new Exception("计时任务无法正常结束");
}
}
} catch (Exception var4) {
var4.printStackTrace();
}
}
});
} catch (Exception var4) {
var4.printStackTrace();
}
}
}
public static boolean taskIsEnd(String serverKey) {
boolean result = cache.containsKey(serverKey);
if (result) {
return cache.get(serverKey).getTaskTime() == 0;
} else {
return false;
}
}
public static synchronized ThreadPoolTaskExecutor getUserThreadPoolTaskExecutor() {
if (executor == null) {
Class var0 = TaskExe.class;
synchronized(TaskExe.class) {
if (executor == null) {
executor = new ThreadPoolTaskExecutor();
int core = Runtime.getRuntime().availableProcessors();
executor.setCorePoolSize(core);
executor.setMaxPoolSize(core * 2 + 1);
executor.setKeepAliveSeconds(3);
executor.setQueueCapacity(40);
executor.setThreadNamePrefix("userService-thread-execute");
executor.setRejectedExecutionHandler(new CallerRunsPolicy());
}
}
}
return executor;
}
}
阅读代码能力好的读者这里也发现了原理很简单,准确性也很难保证,数据一致性无法保证(所有会受到宕机影响的数据一致性操作,都不是真正的数据一致性操作)。
这里有用到TimerHashMap 这种数据结构,也是笔者封装的
public class TimerHashMap implements Map<String,TaskInfo> {
private final static Map<String, TaskInfo> local = new HashMap<>(1000);
public boolean endTaskInCache(String key,TaskInfo taskInfo){
synchronized (local) {
if (local.containsKey(key)) {
boolean remove = local.remove(key, taskInfo);
if (remove) {
taskInfo.setTaskTime(0);
TaskInfo put = local.put(key, taskInfo);
return put == null;
}
return false;
}
return false;
}
}
@Override
public int size() {
return local.size();
}
@Override
public boolean isEmpty() {
return local.isEmpty();
}
@Override
public boolean containsKey(Object key) {
return local.containsKey(key);
}
@Override
public boolean containsValue(Object value) {
return local.containsValue(value);
}
@Override
public TaskInfo get(Object key) {
synchronized (local) {
return local.get(key);
}
}
@Nullable
@Override
public TaskInfo put(String key, TaskInfo value) {
synchronized (local){
return local.put(key, value);
}
}
@Override
public TaskInfo remove(Object key) {
synchronized (local){
return local.remove(key);
}
}
@Override
public void putAll(@NotNull Map<? extends String, ? extends TaskInfo> m) {
synchronized (local){
local.putAll(m);
}
}
@Override
public void clear() {
synchronized (local){
local.clear();
}
}
@NotNull
@Override
public Set<String> keySet() {
synchronized (local){
return local.keySet();
}
}
@NotNull
@Override
public Collection<TaskInfo> values() {
synchronized (local){
return local.values();
}
}
@NotNull
@Override
public Set<Entry<String, TaskInfo>> entrySet() {
synchronized (local){
return local.entrySet();
}
}
}
这段代码里笔者为了保证原子操作,防止互相影响,对更新与删除加入了同步机制
另外也能发现一种对象TaskInfo与一种枚举TaskType
@Data
public class TaskInfo {
private String TaskServerName;
private Integer TaskTime;
private TaskType Type = TaskType.TASK_DELETE_CACHE;
public TaskInfo(String serverName, Integer taskTime, TaskType type){
this.TaskServerName = serverName;
this.TaskTime = taskTime;
this.Type = type;
}
public TaskInfo(){
}
}
public enum TaskType {
TASK_DELETE_CACHE,
TASK_INTER_CACHE,
TASK_SUPER_CACHE;
private TaskType() {
}
}
以及TimerCacheFactory实现的TimerHashMap的全局唯一性
public class TimerCacheFactory {
private static volatile TimerHashMap cache = null;
public TimerCacheFactory() {
}
public static TimerHashMap getCache() {
if (cache == null) {
Class var0 = TimerCacheFactory.class;
synchronized(TimerCacheFactory.class) {
if (cache == null) {
cache = new TimerHashMap();
}
}
}
return cache;
}
public static void releaseCache() {
if (cache != null) {
cache.clear();
}
}
}
笔者最近下班回家也就做了这些,谨以此文,缅怀我少睡的那些夜晚,少做的那些好梦。不管怎么说,将所思所想付诸于实践之上能获得更多的成长,心性或是技术,阅历或是认知。
照影舒月,春花渐明,四月人愁思,也不过三两春风。
可叹光阴偷渡,可叹终日忙碌。