jfinal中使用过滤器监控Druid的SQL执行
从事生产活动,是人的认识发展的基本来源。——《实践论》
从事生产活动,是人的认识发展的基本来源。——《实践论》
通过滚动的高度来判断是否到达了底部
若依系统实现了以下数据权限:
- 全部数据权限
- 自定义数据权限
- 部门数据权限
- 部门及以下数据权限
- 仅本人数据权限
如何解决错误:Illegal mix of collations (utf8mb4_general_ci,IMPLICIT) and (utf8mb4_unicode_ci,IMPLICIT) for operation '='
本文讨论一个问题:
存储token时,token与对应用户id谁来作为key?
问题起源于要给公司的后台管理系统添加权限管理,选用的是开源框架shiro,而原本系统上是采用token做了登录校验的。
我所采用的shiro验证方式是,每次接口请求,根据token来获取用户id,然后通过shiro中的登录验证机制来进行权限校验。
因此,“根据token获取用户id”就要求在存储用户token时,以token为键值key,以用户ID为value值。
然而此时面临一个问题是,系统原本的token存储方式如下,我们称之为第一种:用户ID为key。
1 | cache.set(TOKEN_PREFIX + userid, token); |
这就需要我做出判断,需不需要修改token的存储方式为下面的形式:我们称之为第二种:token为key。
1 | cache.set(TOKEN_PREFIX + token, userid); |
我们需要实现的功能包括:
对于”用户ID为key”的方式,需要前端传递用户id+token两个值,验证登录状态需要我们根据前端传递的用户ID,获取数据库中存储的token,与前端传递的token进行校验,如果一致,则校验通过,否则返回错误信息,提示用户需要重新登录等等。
对于“token为key”的方式,前端至少需要传递token一个值,根据前端传递的token,获取数据库中存储的用户ID,如果能获取到,则校验通过,否则提示用户token已过期,需要用户重新登录等等。
shiro中的权限验证,涉及到具体的实现机制,以token为key的方式,就以我们的真实实现为例:
1 |
|
如果采用userid为key的方式,不难实现,也修改其实现方式,
第一种方式需要前端每次请求都传递token+userid;而第二种实际上可以只传递token,后台根据token解密(或数据库查找)来获取用户信息。
两种方式的安全应该是一样的,核心是后台通过数据库保存token与userid的对应信息。
个人比较细化第二种,以token为key的方式,首先,前端传递简单,只需要传递token即可;二是后端通过这种方式,可以统一当前登录人的获取方式,而不是每次在接口中获取header中的用户id。
一种很常见的网站优化,就是在客户进行危险操作的时候,让客户进行二次确认,从而避免因为用户一时手误,如图所示:
这种方式无须多言,实现起来逻辑并不困难,难在这种方式代码量比较大,首先需要在HTML部分创建对话框代码:
1 | <el-dialog |
这种方式虽然工作量大,但是有一个比较好的优点是,可以比较方便的调整提示操作的格式,一种普遍存在的场景是,用户在修改信息的时候需要核对修改的数据与原数据的区别,那么使用这种方式就可以很好的实现。
该种方式实现起来非常简单,可以单纯的通过Javascript来达到提示语的目的,如本文的的第一张图就是使用该种方式的截图。
1 | this.$confirm('确认删除数据吗?', '提示', { |
el-popconfirm
气泡确认框,这是一个Element-UI的组件,实现完成的样式如下。
一个简单的样例是:
1 | <template> |
el-popconfirm
气泡确认框的详细文档见Element-UI官方文档。
如果运营经理或者产品经理或者客户要求,能够细致的展示提示信息,而不是仅仅是一个操作确认的话,我们似乎只能选用第一种自定义对话框的方式;
相反,如果仅仅是为了避免用户误操作或者避免因为种种原因误删除数据,我们可以采用第二种JS确认框或者第三种气泡确认框,第三种需要修改html样式,第二中则只需要修改js即可。
还是那句话,具体看业务需求吧!
祝大家早日脱离CRUD Developer!
经典问题:在测试环境好好的,怎么到正式环境就不行了?
——数据量变了。
最近在开发公司的后台管理系统,很简单的一个部分,给部门设置领导,前端选用Element-UI的 el-select
组件,后端返回的可选人员列表为当前操作人有权控制的每一个人。
在测试环境运行好好的,怎么到了正式环境就不行了呢?
出问题的原始代码很简单:
1 | <el-select |
问题出在测试环境时,el-select的可选数据 leaderOptions
的量级顶多100多人,而到了正式生产环境中,leaderOptions
达到了将近两万左右,从而造成了组件卡顿。
因此,解决问题的方式就是,如何降低el-select的可选项 leaderOptions
的数量。
我们认为有两种解决思路:
这种方式的优点是,思路清晰简单,修改后台接口的返回数据量,每次查询只返回100条数据,毕竟从使用上来说,不会真的有人一直下拉来选择。
这种方式的缺点是,出现问题的是前端,却需要后端来解决,增加了后端的工作量,或者说,将一个领域的问题扩展到另一个领域的人员去解决,会增加团队沟通成本。
因此,我们放弃了这种方式。(其实就是懒得沟通,就想自己解决问题,不麻烦别人!)
为了减少 el-select
的可选项数量,我们构造数据:leaderOptionsTop30
, 每次只返回所有可选项的(大约)30条数据,那么这大约30条数据是根据什么来筛选获的呢?
我们设置el-select的 filter-method
:
1 | <el-select |
方法体:
1 | filterCheckPerOptions(query = '') { |
打完,收工!
正式环境运行与测试环境运行结果不同,通常有很多原因,例如:
1
学习ThreadLocal起源于最近学习的两个框架:若依开源系统,以及权限验证的开源框架Shiro。
在若依开源系统中,其分页插件:PageHelper的部分核心代码中,有:
1 | package com.github.pagehelper.page; |
而shiro核心版块中有:
1 | public abstract class ThreadContext { |
其中,都用到一个非常强大的类:ThreadLocal。
在PageHelper中,使用TheadLocal来保存分页参数Page
,在Shiro中,使用ThreadLocal保存了一个map<Object, Object>
对象Resources
,根据上下文可以看出shiro中的ThreadLocal保存了当前登录账号,或者说当前登录对象的信息,即shiro中的核心对象subject。
ThreadLocal给予了我们为当前线程局部保存变量的能力,换一个角度,ThreadLocal可以做到线程的数据隔离。
只有特定线程能取出特定线程的数据,这一点我们很容易联想到ThreadLocal的实现原理:构造一个Map映射对象,以Thread为键值key,以特定的值为存储的值,从而实现每次取值只能取当前线程保存的值。
ThreadLocal的两个重要的api为get,set,可以通过其源码看到这一原理的实现方式:
1 | public void set(T value) { |
其中构造了一个特定的映射对象ThreadLocalMap,我们不再赘述。
ThreadLocal可以存储线程执行上下文信息,简化一些线程中方法调用栈参数逐层传递的问题,比如我们在文章《拦截器中巧用ThreadLocal规避层层传值》中提到的,使用ThreadLocal保存请求Request,从而在接口内部可以直接获取当前请求信息,利用这一点可以做很多事情,例如我们可以抛开shiro做自己定制的权限验证系统。
ThreadLocal也解决了某些线程不安全问题,例如时间日期格式化类SimpleDateFormate是非线程安全的,我们通过ThreadLocal来设置则规避了这一点:
1 | public static final ThreadLocal<DateFormat> df_yyyy_MM_dd = |
当ThreadLocal与线程池一起使用时,便会有一个问题,当从线程池取出一个线程后并归还后,下次从线程池中取同样的线程,保存在ThreadLocal中的值是否会泄露?
不谈实验,根据我们查找的资料,对这个问题有两种看法,一种说法是ThreadLocal中使用的键值并非是Thread,而是线程Thread的弱引用,当线程回收时会触发垃圾回收机制,并不会造成数据泄露;更多的资料反应存在内存泄露问题以及生产环境发生过的惨案,如记一次Java线程池与ThreadLocal引发的血案以及An Introduction to ThreadLocal in Java。
更准确地说,当ThreadLocal的修饰符有static时,即强引用的ThreadLocal需要我们手动使用remove方法来释放数据,为了养成良好的习惯,建议还是当ThreadLocal使用结束后,就调用其remove方法。
最近,准确地说,是一直都有的一点麻烦事:函数层层传递。什么意思呢?比如说有个很常见的需求描述是:记录用户的某次操作明细。
以在Java的开源框架jfinal中,操作添加一个用户的接口为例,有:
1 | public class XXController() { |
为了记录用户添加的具体操作内容以及信息错误,这个记录用户操作的记录可能需要穿透好层方法。
1 | public class XXService() { |
举得这个例子或许不是很恰当,但是相信大家可以理解这个麻烦点在哪里。
函数调用链中的每个方法并不是需要这个参数,而可能仅仅是为了向下个被调用的函数传递这个参数。
那么是否有方法帮助我们来不需要这样逐层传递,从而获取接口请求参数的方法呢。
这有点类似与一个全局变量,但是这个变量对每次请求来说是变化的,那么是否有一种方式能够让我们来保存这样一种:针对每次请求的全局变量呢?
在最近学习Shiro的过程中,以及学习若依开源框架的过程中,我们发现其均使用到了一个强大的Java类:ThreadLocal。
shiro使用ThreadLocal是用来保存当前登录对象,若依框架中,其中所使用的分页插件PageUtil使用ThreadLocal保存请求参数中的pageNum与pageSize等分页参数。因此我们是不是也可以使用ThreadLocal来达到同样的目的。
为此,我们来尝试一下,通过ThreadLocal保存请求参数,通过拦截器我们可以拦截每次请求,如下是实现方式:
1 | package com.holdoa.core.interceptor; |
我们通过ThreadLocal将整个请求暂存起来,当然,为了节约内存,也可以只保存使用频次高的通用参数,比如当前登录人的信息等等。
使用时,只需要我们通过这个线程局部变量取值即可:
1 | String para = RequestPool.localRequest.get().getParameter("username"); |
如此,我们便可以在处理每次请求的过程中,无需从Controller以及Service中的方法层层传值,只需要直接通过该局部变量取值即可。
学习了Java的代理,动态代理,cglib的小伙伴对Java的代理中的一些概念应该会有一些基础的认识,比如如何使用代理实现一个拦截器,如果通过cglib的回调过滤器来修改方法的执行逻辑等等,那么这种实现方式是否可以抽象为更高级的编程思想呢?使用cglib创建对象的过程与代码很繁琐,是否有更便捷的开源包可以使用呢?
AOP:面向切面编程,其思想是:
通过预编译方式和运行期动态代理方式,实现在不修改源代码的情况下给程序动态统一添加额外功能的一种技术
我们今天要学习的AspectJ就是AOP思想的一个工具包,定义了AOP语法与基本概念。
连接点:JoinPoint:
切入点:PointCut;
通知:Advice;
切面:Aspect。
其官方文档释义如下:
A join point is a well-defined point in the program flow. A pointcut picks out certain join points and values at those points. A piece of advice is code that is executed when a join point is reached. These are the dynamic parts of AspectJ.
其含义如下图所示:
让我们从实际代码触发来理解这几个概念。
使用aspectj需要我们引入以下两个maven包:
1 | <!-- https://mvnrepository.com/artifact/org.aspectj/aspectjrt --> |
第一个aspectjrt是aspectj运行时需要的库(Runtime Library),第二个aspectjweaver,支持切入点表达式,用于在加载Java类时起作用。
(在学习的过程中,有个东西始终让我无法习惯,就是在java中引入.aj
文件)
需要习惯这一点,在非spring项目中,使用aspectj可能需要修改maven文件与创建非.java
文件。不过,现在的编译器可以帮助我们习惯这一点,maven引入aspectj之后,IDEA便有了创建aspectj文件的选项:
最开始,我们有以下类:
1 | package com.qw.real.aop; |
然后创建aspectj文件:
1 | public aspect AjAspect { |
使用aspectj与其他java程序最大的不同是,需要特殊编译环境,我们以idea为例,除需要在maven中引入之前的两个aspectj运行环境,还需要引入一个aspectj编译器:
1 | <!-- https://mvnrepository.com/artifact/org.aspectj/aspectjtools --> |
我们配置Java Compiler,测试:
这个错误似乎是在说我们的编译环境与运行环境所使用的版本不一致:编译用version 55,而运行使用的是 version 52,猜测可能是因为我们使用的最新的aspect包,为此,降低aspectj版本到1.8.3再次尝试:
测试成功!
然后我们去执行:
1 | before... |
完结,鼓掌!
总的来看,aspectj非常强大,但是其需要特殊配置编译环境这一点可能不太能被人接受。