1

问题缘起

在学习开源框架jfinal的过程中,尤其是研究其拦截器的实现时,遇到AOP的问题,然后逐步引导着自己学习Java的动态代理,认识到一个强大的工具包:cglib。

在上一篇文章《使用cglib创建Java代理以及调用的结果分析》中,我们学习了cglib创建Java代理的实现方式,通过Enhancer来创建监听对象,从而对方法进行拦截。

如下为main方法中,对Writer类进行动态代理的过程。其中WriterInterceptor实现了net.sf.cglib.proxy.MethodInterceptor接口

1
2
3
4
5
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Writer.class);
enhancer.setCallback(new WriterInterceptor());
Writer writer = (Writer) enhancer.create(new Class[]{String.class}, new String[]{"洛叶飘"});
writer.doWork();

在学习cglib的过程中,遇到一个新的概念:回调函数,以及回调过滤器。

回调

Java中也有回调,但是笔者个人碰到回调这个词,第一个想到的是javascript中的回调,将函数作为参数传递给另一个函数:如下所示:

1
2
3
4
5
6
7
8
9
10
function myDisplayer(some) {
console.log(some)
}

function myCalculator(num1, num2, myCallback) {
let sum = num1 + num2;
myCallback(sum);
}

myCalculator(5, 5, myDisplayer);

而Java中的回调是怎么回事呢?

我们知道Java中参数不能传递函数,那么又是如何实现回调的呢?

如下代码是一种实现回调的逻辑:通过只有一个函数的接口,在对象中保存一个接口对象,在指定方法内部调用接口对象的方法,从而实现回调机制:

1
2
3
4
5
package com.qw.callback;

public interface CallBackIn {
public void callBackMethod();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.qw.callback;

public class Test {
private CallBackIn callBack;

public void setCallBack(CallBackIn callBack) {
this.callBack = callBack;
}
public void call() {
callBack.callBackMethod();
}

public static void main(String[] args) {
Test test = new Test();
test.setCallBack(new CallBackIn() {
@Override
public void callBackMethod() {
System.out.println("回调函数被执行啦...");
}
});
test.call();
}
}

执行结果:

1
回调函数被执行啦...

cglib的回调过滤

借助cglib,可以实现给某个类的所有方法或部分方法设置回调函数,可以实现修改函数返回结果,添加过滤器等功能。

一个实现样例如下:

有要被过滤的类Writer,他它有三个方法,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.qw.cglib;

public class Writer {
String name;

public Writer(String name) {
this.name = name;
}

public String doWork() {
System.out.println(name + "在工作");
return name + "在工作";
}

public String rest() {
System.out.println(name + "在休息");
return name + "在休息";
}

public String eat() {
System.out.println(name + "在吃饭");
return name + "在吃饭";
}
}

回调过滤器的实现,如下,通过实现net.sf.cglib.proxy.CallbackFilter接口,其中的accept方法可以指定设置类的过滤数组时,某个方法在过滤数组中的索引:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.qw.cglibfilter;

import net.sf.cglib.proxy.CallbackFilter;

import java.lang.reflect.Method;

public class TargetMethodCallbackFilter implements CallbackFilter {

/**
* @param method
* @return 返回值为被代理类的各个方法在回调数组Callback[]中的位置索引
*/
@Override
public int accept(Method method) {
System.out.println(method.getName());
if (method.getName().equals("doWork")) {
System.out.println("filter doWork == 0");
return 0;
}
if (method.getName().equals("rest")) {
System.out.println("filter rest == 1");
return 1;
}
if (method.getName().equals("eat")) {
System.out.println("filter eat == 2");
return 2;
}
return 0;
}
}

过滤器使用方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Writer.class);
CallbackFilter callbackFilter = new TargetMethodCallbackFilter();
Callback callback1 = new TargetInterceptor();
Callback noop = NoOp.INSTANCE;
Callback fixedValue = new TargetResultFixed();
Callback[] callbacks = new Callback[]{callback1, noop, fixedValue};
enhancer.setCallbacks(callbacks);
enhancer.setCallbackFilter(callbackFilter);
Writer writer = (Writer) enhancer.create(new Class[]{String.class}, new String[]{"洛叶飘"});
System.out.println("----------------函数调用doWork-----------------");
String workResult = writer.doWork();
System.out.println("----------------函数调用rest-------------------");
String restResult = writer.rest();
System.out.println("----------------函数调用eat--------------------");
String eatResult = writer.eat();
System.out.println("------------------函数返回值-------------------");
System.out.println(workResult);
System.out.println(restResult);
System.out.println(eatResult);

上述代码中,TargetInterceptor是我们上一篇文章中的方法拦截器类,这里不再赘述。NoOp类表明什么操作也不做(可以看看下面的运行结果来体会一下),TargetResultFixed实现了net.sf.cglib.proxy.FixedValue接口,可以修改函数的返回值。

我们先来看看运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
doWork
filter doWork == 0
rest
filter rest == 1
eat
filter eat == 2
equals
toString
hashCode
clone
----------------函数调用doWork-----------------
调用前
洛叶飘在工作
调用后
调用结果:洛叶飘在工作
----------------函数调用rest-------------------
洛叶飘在休息
----------------函数调用eat--------------------
锁定结果
------------------函数返回值-------------------
洛叶飘在工作
洛叶飘在休息
什么都不干

代码补充

TargetResultFixed类的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.qw.cglibfilter;

import net.sf.cglib.proxy.FixedValue;

public class TargetResultFixed implements FixedValue {

@Override
public Object loadObject() throws Exception {
System.out.println("锁定结果");
Object obj = "什么都不干";
return obj;
}
}

问题起源

在最近的工作中,AOP如何实现的问题一直困扰着笔者,奈何工作繁重,一直没有时间学习了解这一块。

这一次,趁着工作上使用shiro的风,顺着shiro注解的使用过程,我们来逐步了解了注解,自定义注解的实现,Java反射,以及在上一篇文章《从使用Java代理的极简代码看Java代理逻辑》中,我们学习了Java的代理。顺着Java代理的学习路线,我们了解到有一个非常遍历的工具包:cglib。

cglib的基础原理

对于cglib的基础原理,我们暂不做深究,但是可以大概概况为:使用字节码处理框架ASM,动态地生成所要代理类的一个子类,子类中重写类的所有非final方法,从而在子类中拦截父类方法,插入相关逻辑代码。

cglib的应用

我们以java的maven项目为例,来看看如何使用cglib的包。

cglib的mvn包分为两个:cglib与cglib-nodep,我们可以在mvn仓库中搜索到,cglib-nodep内部包含asm的类,不需要再额外引入,通常我们使用这个包,使用方式如下:

1
2
3
4
5
6
7
8
<!-- https://mvnrepository.com/artifact/cglib/cglib-nodep -->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib-nodep</artifactId>
<version>3.3.0</version>
<scope>test</scope>
</dependency>

另外,我们惊喜的发现jfinal中也使用了cglib包,相信我们距离jfinal拦截器的实现又接近了一大步。

image-20220421195423922

实现过程

如下,我们有想要被代理的类

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.qw.cglib;

public class Writer {
String name;

public Writer(String name) {
this.name = name;
}

public void doWork() {
System.out.println(name + "在工作");
}
}

方法拦截器类,实现cglib中的接口MethodInterceptor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.qw.cglib;

import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class WriterInterceptor implements MethodInterceptor {

@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("方法调用前");
Object result = methodProxy.invokeSuper(o, objects);
System.out.println("方法调用后");
System.out.println(result);
return result;
}
}

其中有一点,需要我们观测一下,我们想要监测的方法是doWork,它的返回值是void,我们观察一下result的输出结果。

测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.qw.cglib;

import net.sf.cglib.proxy.Enhancer;

public class TestWriter {
public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Writer.class);
enhancer.setCallback(new WriterInterceptor());
Writer writer = (Writer) enhancer.create(new Class[]{String.class}, new String[]{"洛叶飘"});
writer.doWork();
}
}

执行结果:

1
2
3
4
方法调用前
洛叶飘在工作
方法调用后
null

成功的完成了对方法的监控,并插入了相关代码。

注意invokeSuper的返回值

其中,不难注意到,void返回值的打印结果是null。那么,如果返回值为null的,其反射调用结果是多少呢?

我们修改一下doWork方法:

1
2
3
4
public String doWork() {
System.out.println(name + "在工作");
return null;
}

其他部分代码不变,重新执行,结果如下:

1
2
3
4
方法调用前
洛叶飘在工作
方法调用后
null

如此,即无论返回值为void,或者实际返回的null,反射方法调用的返回结果均为null

问题起源

最近受到注解启发,学习了Java注解的实现方式,以及Java注解执行器中涉及到的Java反射的过程,但是笔者本人更关心的拦截器、过滤器等涉及到的Java AOP还没有涉及到。网上搜索到的很多关于AOP的实现都与Spring做了深度绑定,然而,我有个小癖好,不太喜欢与框架深度绑定的,所以,我们只是简简单单的学习一下AOP的实现方式,而不是Spring AOP的实现方式,因为“我们要做Java Developer,而不是Spring Developer”。

代理

想要实现AOP,先要学习Java中的代理。代理模式的含义是:

给某一个对象提供一个代理,并由代理对象控制对原对象的引用。

代理分为静态代理,动态代理。

静态代理

静态代理的例子代码如下所示,我们不做过多的叙述,具体可以看文中注释。(其实,自己把代码敲一遍就知道能够理解静态代理了。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public interface ApiCall {
void call();
}

/**
* 具体的执行类
*/
public class SaleApiCall implements ApiCall {
private String name;


public SaleApiCall(String name) {
this.name = name;
}

public void call() {
System.out.println(name + "被调用");
}
}

public class SaleApiCallProxy implements ApiCall {
SaleApiCall saleApiCall;

public SaleApiCallProxy(ApiCall api) {
if (api.getClass() == SaleApiCall.class) {
this.saleApiCall = (SaleApiCall) api;
}
}

//代理执行,调用被代理接口的行为
public void call() {
// before 这里可以添加方法执行器的逻辑
saleApiCall.call();
// after 这里可以添加方法执行后的逻辑
}
}

public class StaticProxyTest {
public static void main(String[] args) {
ApiCall qw = new SaleApiCall("订单接口");
ApiCall proxy = new SaleApiCallProxy(qw);
proxy.call();
}
}

输出结果:

1
订单接口被调用

动态代理

动态代理分为JDK动态代理与使用cglib的动态代理,前者需要被代理对象实现接口,而后者不需要。我们先来看看如何使用JDK来完成动态代理。

理解下面代码代码“动态”的核心观念在于,我们没有修改所要代理的对象内部,而是通过外部代理类来获取对象与执行方法。

例如,原本有一个接口Person与一个实现接口的类Programmer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public interface Person {
void work();
}

public class Programmer implements Person{
String name;

public Programmer(String name) {
this.name = name;
}

@Override
public void work() {
System.out.println(name + "进行编程工作");
}
}

现在,我们希望在不改变原内容的情况下,通过代理方式生成Programmer对象并对其执行前后插入指定的行为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ProgrammerInvocationHandler<T> implements InvocationHandler {
T target;

public ProgrammerInvocationHandler(T target) {
this.target = target;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method.getName() + "方法执行前");
Object result = method.invoke(target, args);
System.out.println(method.getName() + "方法执行后");
return result;
}
}

上述类实现了接口InvocationHandler,这个接口中有一个方法invoke,我们可以从invoke的内部代码大致猜测到其执行逻辑,尤其是method.invoke(...)这一句。

1
2
3
4
5
6
7
8
public class DynamicProxyTest {
public static void main(String[] args) {
Person realPerson = new Programmer("洛叶飘");
InvocationHandler programmerInvocationHandler = new ProgrammerInvocationHandler<Person>(realPerson);
Person lyp = (Person) Proxy.newProxyInstance(Person.class.getClassLoader(), new Class<?>[]{Person.class}, programmerInvocationHandler);
lyp.work();
}
}

如上所示,便通过Invocation与Proxy生成了监听的对象lyp,这个对象的所有方法执行,都会触发ProgrammerInvocationHandler中的invoke方法。

上述方法的执行结果如下:

1
2
3
work方法执行前
洛叶飘进行编程工作
work方法执行后

这种方法需要我们的类Programmer来实现一个接口Person,那么又没有更简单的方式呢?

有的,那就是使用cglib包。

问题起源

最近我们在研究注解,由注解引入,我们“被迫”去学习Java中的反射,反射学习的差不多了,我们再回过头来看看如何去处理注解。

今天,我们先说一下注解学习过程中的一点收获:注解,仅仅只是标注,具体让程序怎么理解注解,仍然需要我们自己来实现。

为了让问题和结论更清晰,我们来自己实现一个注解。

自定义注解

我们首先使用@interface定义了一个注解:我们内心给予这个注解的含义是,定义在类中属性域上,其值value表示转换为字符串时的名称,然而注解定义不包括执行解释,但从定义上看不到任何执行逻辑。

1
2
3
4
5
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface SpName {
public String value() default "";
}

重点在于注解的使用与执行:如下,我们定义了一个类Animal,其中给一个属性name标注了注解@SpName("animal-name"),然后,我们在其中toString方法中来解释注解,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.qw.anno;

import com.jfinal.kit.LogKit;

import java.lang.reflect.Field;

public class Animal {

@SpName("animal-name")
public String name;
public int age;

@Override
public String toString() {
StringBuilder result = new StringBuilder();
for (Field field: this.getClass().getDeclaredFields()) {
field.setAccessible(true);
try {
if (field.isAnnotationPresent(SpName.class)) {
String annotationValue = field.getAnnotation(SpName.class).value();
result.append(annotationValue).append(":").append(field.get(this)).append(";");
} else {
result.append(field.getName()).append(":").append(field.get(this)).append(";");
}
} catch (IllegalAccessException e) {
System.out.println("异常");
}
}
return result.toString();
}
}

代码编写完后,我们先来验证一下程序:

1
2
3
4
Animal animal = new Animal();
animal.age = 15;
animal.name = "牛牛";
System.out.println(animal.toString());

执行结果:

1
animal-name:牛牛;age:15;

成功执行!

关键代码

获得注解执行的关键代码在于,通过反射获取到类中所有的域:

1
for (Field field: this.getClass().getDeclaredFields()) 

以及,遍历域的过程中,判断当前域是否被添加了SpName注解:

1
2
3
if (field.isAnnotationPresent(SpName.class)) {
....
}

而获取注解的值的代码为:

1
String annotationValue = field.getAnnotation(SpName.class).value();

如此,我们便能够按照自定义的方式来解释执行注解,从而达到自定义注解的方式。

我所一直纠结的地方

其实,我心理一直想要的注解形式是如下的形式:

1
2
3
4
@BeforeOrAfterAction("")
public void method() {
....
}

其中,注解BeforeOrAfterAction表示在函数执行前后做某些指定工作,比如做方法执行的权限检查等等。其实在慢慢寻找答案的过程中,我们发现,这个问题并不属于注解的解决范畴,而确切的说法是AOP:面向切面编程来实现这种思路,常见的包括Spring AOP,以及各个程序中见到的拦截器都是这种实现思路。

问题起源

最近在学习shiro,shiro中一个很便利很优雅的地方便是可以给方法添加注解,以便控制当前接口内部是否能够被当前登录用户访问。

shiro控制样例代码如下:

1
2
3
4
@RequiresPermissions("all")
public void test() {
renderText("测试");
}

这行代码的作用是:限制只有拥有all权限的用户才可以访问该接口,否则会抛出异常。

那么问题来了,如何实现Java中非常优雅的注解这种形式呢?

注解的实现原理

注解的实现是基于反射原理的。

Talk is Cheap, Show me your Code!

一个注解的实现过程

下面我们将逐步尝试实现一个我们自己的注解,比如我们叫它MyAnno,他的作用就是在其注解的方法执行前输出一段指定的文字。

实现注解,首先需要有一个被标记为@interface的接口。

1
2
3
public @interface MyAnno {

}

在Java中还有四种注解被称为元注解,即Java库帮助我们实现的注解,分别有TargetRetentionDocumentedInherited

分别表示:

@Target

修改其他注解类,标注当前注解会被用在什么地方,使用样例如:@Target({ElementType.TYPE, ElementType.METHOD})。括号中可选的参数可以参考java.lang.annotation.ElementType,我们说几个例子:

ElementType.TYPE:表示类注解,类型注解
ElementType.FIELD:字段注解
ElementType.METHOD:方法注解
ElementType.PARAMETER:参数注解
ElementType.CONSTRUCTOR:构造方法注解
ElementType.LOCAL_VARIABLE:局部变量注解
ElementType.ANNOTATION_TYPE:注解的注解
ElementType.PACKAGE:包注解

比如,我们准备定义的MyAnno为方法注解,则有:

1
2
3
4
@Target(ElementType.METHOD)
public @interface MyAnno {

}

@Rentention

表示当前注解的运行状态,可取值参见java.lang.annotation.RetentionPolicy

RetentionPolicy.SOURCE:其注释为将被编译器丢弃,只在源码运行;

RetentionPolicy.CLASS:编译类文件是运行;

RetentionPolicy.RUNTIME:运行时运行。

比如,我们定义的MyAnno,需要添加代码如下:

1
2
3
4
5
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyAnno {

}

@Documented

该注解为生成说明文档,添加类的解释,不多说,我们暂且略过。

@Inherited

表示允许子类继承父类中的继承,我们也暂且略过。

到这里我们的注解就定义完了。我们来尝试使用以下,仅仅是使用,还没到运行的时候。

1
2
3
4
5
6
7
8
9
10
11
public class TestAnno {

@MyAnno
public void testAnno() {
System.out.println("testAnno方法内部");
}

public static void main(String[] args) {
new TestAnno().testAnno();
}
}

这段代码可以编译通过,没有报错,并且执行也没有问题,当然,现在我们的注解还没有添加动作。

那么,怎么给注解添加指定动作呢?

最最关键的内容:注解执行器

注解执行器才是注解中最关键的内容,上面的代码仅仅是告诉我们,告诉编译器与jvm,这个方法被我们标记了。但是具体怎么执行,还是要看注解执行器的。但是执行器设计到反射,复杂的执行器还涉及到AOP编程,我们慢慢展开来讲。

对于支持注解的强迫症

在上一篇文章jfinal中stateless模式嵌入shiro验证中我们已经成功嵌入了shiro,但是呢,有个小缺陷,并没有支持shiro的注解,例如,如下方式并不能触发权限验证:

1
2
3
4
@RequiresPermissions("all")
public void test() {
renderText("测试");
}

在我们看来,以及在《Effective Java》的作者看来,注解都是非常优雅的一种实现方式。如果不使用注解,要实现上述代码的功能,需要怎么做呢?

不使用注解的方式

如果不使用注解,就需要我们在代码中具体指明其执行逻辑,就上面的例子来说,没有注解而具有同样功能的代码如下:

1
2
3
4
5
public void test() {
Subject s = SecurityUtils.getSubject();
s.checkPermission("all");
renderText("测试");
}

即需要我们手动的来获取对象以及执行对象的检查权限方法。

对于shiro注解的支持

这里我们找到了jfinal-shiro-plugin是支持注解的,因此,首先我们按照jfinal-shiro-plugin的方式,将其所有代码搬移过来。(在文章jfinal中stateless模式嵌入shiro验证中已经阐述了搬移的原因:即jfinal4.8对于jfinal-shiro-plugin无法兼容,执行报错)

在搬移了其源码后,首先按照jfinal-shiro-plugin的方式,在程序入口进行配置,包括拦截器与插件,具体代码如下:

1
2
3
4
5
6
7
/**
* 配置全局拦截器
*/
public void configInterceptor(Interceptors me) {
// 这里可能还会有别的拦截器
me.add(new ShiroInterceptor());
}

配置插件:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 配置插件
*/
public void configPlugin(Plugins me) {

// 这里可能还会有数据库,redis,定时等插件
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
org.apache.shiro.mgt.SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);

ShiroPlugin shiroPlugin = new ShiroPlugin(this.routes);
me.add(shiroPlugin);
}

上面代码中shiroPlugin构造函数中的routes需要我们对程序入口做一点小小的改造:

1
2
3
4
5
6
7
8
9
10
11
Routes routes = null;

/**
* 配置路由
*/
public void configRoute(Routes me) {

me.add("/", IndexController.class, "/index");
// 其他路由
this.routes = me;
}

核心:每次调用首先执行subject.login()

需要对shiro的拦截器做一点小小的修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void intercept(Invocation ai) {
/////////////////自己添加的代码
String token = ai.getController().getHeader("token");
if (StrKit.notBlank(token)) {
Subject s = SecurityUtils.getSubject();
JWTToken jwtToken = new JWTToken(token);
s.login(jwtToken);
}
//////////////////

AuthzHandler ah = ShiroKit.getAuthzHandler(ai.getActionKey());
// 存在访问控制处理器。
if (ah != null) {
Controller c = ai.getController();
try {
// 执行权限检查。
ah.assertAuthorized();
}
}
// 这里省略了ShiroInterceptor的其他代码
}

我们首先在拦截器入口添加一个获取token已经调用shiro登录的方法,以便后续shiro能够成功的获取到当前登录对象,以及做权限验证等等。

这里的验证方式完全可以自定义,我们所使用的方式是登录的时候获取完全随机的token串,并将其设置到redis中,每次验证登录,从redis中根据token获取对象信息。

当然,也可以参考JWT方式,对登录人信息加密与解密来获取用户信息。

1

问题起源

在前些天的文章中,我们了解到困惑了我们好几天的问题是由于jfinal新版中使用undertowServer方式启动,其嵌入filter的方式有变动,所以导致网上检索到的通过web.xml嵌入filter失败。

在不考虑修改undertowServer的情况下,也就意味着我们需要找到一种在undertowServer环境下,嵌入shiro的方式。

今天,我们就来尝试一种通过拦截器来实现的Stateless Jfinal 嵌入方式。

Stateless的理解

个人对Stateless的理解就是前后端分离,两次请求互相独立,通过约定的token等内容判断是否是同一个用户。

因此这要求,登录接口需要给用户生成一个随机的token,以便用户后续访问的时候带上。

登录接口

登录接口首先需要我们访问数据库,以及通过特定算法来验证用户名与密码是否匹配。如果匹配,则生成随机的字符串,即token,并保存在redis中,注意,映射关系是token为key,value为用户信息,可以是用户名,也可以是用户id等用户唯一标识。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Clear
public void Login() {
String name = getPara("name");
String password = getPara("password");
if ("admin".equals(name)) { // TODO 判断密码与用户名是否正确
Cache cache = Redis.use();
String token = StrKit.getRandomUUID();
cache.set("TOKEN:" + token, name);
renderText(token);
} else {
renderText("用户名与密码错误");
}
}

另外,需要注意的有两点:

  1. 接口前调用@Clear,即登录接口不应该被拦截验证
  2. 系统的登录接口,与shiro中的subject.login应该注意区分,是两个不同的概念。

自定义拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package com.holdoa.core.interceptor;

import com.holdoa.core.controller.BaseController;
import com.holdoa.core.filter.JWTToken;
import com.jfinal.aop.Interceptor;
import com.jfinal.aop.Invocation;
import com.jfinal.core.Controller;
import com.jfinal.kit.LogKit;
import com.jfinal.kit.StrKit;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.aop.MethodInvocation;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.aop.AnnotationsAuthorizingMethodInterceptor;
import org.apache.shiro.subject.Subject;

import java.lang.reflect.Method;

public class MyShiroInterceptor extends AnnotationsAuthorizingMethodInterceptor implements Interceptor {

public MyShiroInterceptor() {
getMethodInterceptors();
}

public void intercept(final Invocation inv) {
try {
String token = inv.getController().getHeader("token");
if (StrKit.isBlank(token)) {
BaseController b = (BaseController) inv.getController();
b.renderAppError("缺少token");
return;
} else {
Subject s = SecurityUtils.getSubject();
JWTToken jwtToken = new JWTToken(token);
s.login(jwtToken);
inv.invoke();
}
} catch (Throwable e) {
if (e instanceof AuthorizationException) {
doProcessuUnauthorization(inv.getController());
}
LogKit.warn("权限错误:", e);
try {
throw e;
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}
}

/**
* 未授权处理
*
* @param controller controller
*/
private void doProcessuUnauthorization(Controller controller) {
controller.redirect("/login/noLogin");
}
}


上面的代码很长,我们重点看其中的这几行:

1
2
3
4
5
6
7
8
9
10
11
String token = inv.getController().getHeader("token");
if (StrKit.isBlank(token)) {
BaseController b = (BaseController) inv.getController();
b.renderAppError("缺少token");
return;
} else {
Subject s = SecurityUtils.getSubject();
JWTToken jwtToken = new JWTToken(token);
s.login(jwtToken);
inv.invoke();
}

逻辑可以描述为:获取token,若不为空,将其转换为JWTToken对象,然后调用shiro的登录接口:s.login(jwtToken)

而shiro的login方法会触发自定义Realm中的验证接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 自定义认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
String token = (String) auth.getCredentials();
// 解密获得username,用于和数据库进行对比
String userName = JwtUtils.getUsername(token);
if (userName == null || userName == "") {
throw new AuthenticationException("token 校验失败");
}
return new SimpleAuthenticationInfo(token, token, getName());
}

其中,JwtUtils。getUsername的具体代码如下,和设置token是对应的:

1
2
3
4
5
6
7
8
/**
* @return token中包含的用户名
*/
public static String getUsername(String token) {
Cache cache = Redis.use();
String username = (String)cache.get(RedisKeyPreFix.NEW_OA_MANAGE_TOKEN_PREFIX + token);
return username;
}

如此,便做到了shiro的嵌入。

遗留问题

目前欠缺的一个问题是,不能实现shiro的注解来进行权限验证,这个问题我们还准备借助ShiroPlugin来实现,由于jfinal已经升级到4.8了,而shiroPlugin目前还停留在支持jfinal 3.x的版本,所以需要我们下载jfianl-shiro-plugin源码做一些修改。

K-近邻算法的缺点

在前几篇文章中,我们介绍了K-近邻算法。K-近邻算法的最大缺点是无法给出数据的内在含义。

决策树

而今天要学习的决策树算法的一大优势就在于其数据形式非常容易理解。决策树是处理分类问题中最经常使用的数据挖掘算法。

决策树解决问题的一般流程如下:

(1) 收集数据:可以使用任何方法。

(2) 准备数据:树构造算法只适用于标称型数据,因此数值型数据必须离散化。

(3) 分析数据:可以使用任何方法,构造树完成之后,我们应该检查图形是否符合预期。

(4) 训练算法:构造树的数据结构。

(5) 测试算法:使用经验树计算错误率。

(6) 使用算法:此步骤可以适用于任何监督学习算法,而使用决策树可以更好地理解数据的内在含义

决策树算法需要一步一步的构造数据集的子集合。在这个过程中,需要计算划分子集合过程中的信息增益,从而每次划分时,都是的信息增益最大。

在划分数据集之前之后信息发生的变化称为信息增益

为了计算划分集合的信息增益,我们先来学习一下,如何计算一个结合的熵。

计算数据集的熵

一个数据集的概率可以阐述为:集合中某个分类的概率为p(xi),且集合中共有n个分类,则集合的熵为:

H = - ∑n 1 p(xi)log2(p(xi))

p(xi) = 集合中该分类的数量 / 集合实例总数量

如此,我们便能够计算集合的熵。

划分数据集

按照什么样的策略来划分数据集合呢?我们按照指定某个特征值符合某一规则,或者某个特征值等于某个值时,我们将其按照这样的规则来划分。

比如有数据[1, 2, 3, 男], [1, 3, 4,男], [2, 2, 4, 女],其中前三列为特征值,最后一列为分类。比如,我们可以按照第一列特征等于1来划分,如此便得到两个集合:[[1, 2, 3, 男], [1, 3, 4,男]], 以及集合 [[2, 2, 4, 女]]。

如何判断此时划分数据集是否正确呢,或者是否能够使得信息增益最大呢?这就需要逐个的按照每个特征都来划分一次,每次都计算出划分后的每个子集的熵x以及子集在父集合中的概率p,子集个数为n,父集合的熵为H,则使用第i个特征划分集合的信息增益为:

Gi = H - ∑(1-> n) p * x

如此,计算出使得Gi最小的i值,即为当次划分集合最好的特征,即第i列特征。

注意事项

数据集的最后一列为类别标签,每一行都有相同多的数据长度。

1

问题起源

最近借鉴开源管理系统若依(http://www.ruoyi.vip/)开发公司的管理系统,尤其是其使用VUE的前端。在借鉴若依用户管理时遇到一个很怪的BUG。这个bug不能准确复现,但是希望通过这次问题阐述帮助整理清楚问题原因。

问题定位

在开发用户管理界面,编辑已有用户账号时,其操作界面如下:

image-20220414204621423

这次,我们遇到的问题是角色选项时,虽然返回的数据可以自动选中之前已经选择的角色,但是无法勾选新角色,也无法取消已选角色,这让我们陷入沉思,明明前端代码是一样的啊?为什么若依系统中可以,而自己的系统中就不可以呢?

关键代码

我们来看一下这个对话框的代码,

1
2
3
4
5
6
7
8
9
10
....
<el-select v-model="form.roleIds" multiple placeholder="请选择">
<el-option
v-for="item in roleOptions"
:key="item.roleId"
:label="item.roleName"
:value="item.roleId"
></el-option>
</el-select>
.....

我们剔除掉无关紧要或者与本次问题肯定无关的其他代码部分,出问题的便是上方这个el-select组件。el-select组件官网:https://element.eleme.cn/#/zh-CN/component/select。文档中也并没有特别著名什么。因此,我们也正常书写这段代码。其中的`form.roleIds`格式为数组,data中,有:

1
2
3
4
5
6
7

data() {
return {
// 表单参数
form: {}
}
}

获取用户已有角色的接口方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/** 修改按钮操作 */
handleUpdate(row) {
this.reset();
const userId = row.userId || this.ids;
// getUser为接口
getUser(userId).then(response => {
this.form = response.data; // 现有用户数据
this.roleOptions = response.roles; // 获取所以角色权限
this.form.roleIds = response.roleIds; // 获取用户已有选项
this.open = true; // 打开对话框
this.title = "修改用户"; // 对话框标题
this.form.password = ""; // 不修改密码
});
},
reset() {
this.form = {
userId: undefined,
deptId: undefined,
userName: undefined,
nickName: undefined,
password: undefined,
phonenumber: undefined,
email: undefined,
sex: undefined,
status: "0",
remark: undefined,
postIds: [],
roleIds: []
};
this.resetForm("form");
},

我们获取用户数据方法也基本相同,准确的说,没有什么不同,但是我们的仍然是不可选的。

在网络检索“el-select”无法选中问题后,我们尝试了一种可行的方法。

解决问题

一种说法是在form初始化时,其中的roleIds并没有被添加到vue的自动监听机制中,所以其值变化并没有引起el-select的视图刷新。但是,经过我们对form数据进行watch监听,form也并没有发生改变。

尽管如此,我们仍然尝试了文中给出的解决办法:使用this.$set(this.form, 'roleIds', newValue)设置已有角色,如下所示:

1
this.$set(this.form, 'roleIds', response.result.data.roleIds)

如此,竟然成功的解决了问题。

总结

最终,我们猜测,仍然是由于form.roleIds没有被vue自动监听机制发现,所以el-select无法做到视图与数据的更新。

我们可以手动使用this.$set来解决该问题。