🥪 拦截器
用过Spring MVC的朋友一定对Spring的拦截器并不陌生,Forest也同样支持针对Forest请求的拦截器。
如果您想在很多个请求发送之前或之后做一些事情(如打印日志、计数等等),拦截器就是您的好帮手。
# 构建拦截器
定义一个拦截器需要实现com.dtflys.forest.interceptor.Interceptor接口
public class SimpleInterceptor<T> implements Interceptor<T> {
    private final static Logger log = LoggerFactory.getLogger(SimpleInterceptor.class);
    /**
     * 该方法在被调用时,并在beforeExecute前被调用 
     * @Param request Forest请求对象
     * @Param args 方法被调用时传入的参数数组 
     */
    @Override
    public void onInvokeMethod(ForestRequest req, ForestMethod method, Object[] args) {
        log.info("on invoke method");
        // req 为Forest请求对象,即 ForestRequest 类实例
        // method 为Forest方法对象,即 ForestMethod 类实例
        // addAttribute作用是添加和Forest请求对象以及该拦截器绑定的属性
        addAttribute(req, "A", "value1");
        addAttribute(req, "B", "value2");
    }
    /**
     * 在请求体数据序列化后,发送请求数据前调用该方法
     * 默认为什么都不做
     * 注: multlipart/data类型的文件上传格式的 Body 数据不会调用该回调函数
     *
     * @param request Forest请求对象
     * @param encoder Forest转换器
     * @param encodedData 序列化后的请求体数据
     */
    @Override
    public byte[] onBodyEncode(ForestRequest request, ForestEncoder encoder, byte[] encodedData) {
        // request: Forest请求对象
        // encoder: 此次转换请求数据的序列化器
        // encodedData: 序列化后的请求体字节数组
        // 返回的字节数组将替换原有的序列化结果
        // 默认不做任何处理,直接返回参数 encodedData
        return encodedData;
    }
    /**
     * 该方法在请求发送之前被调用, 若返回false则不会继续发送请求
     * @Param request Forest请求对象
     */
    @Override
    public boolean beforeExecute(ForestRequest req) {
        log.info("invoke Simple beforeExecute");
        
        // 获取全局变量或参数中 @Var("变量名") 修饰的变量
        Object var1 = req.variableValue("变量名");
        // 获取 Query 变量
        String query1 = req.getQuery("Query变量名");
        
         // 执行在发送请求之前处理的代码
        req.addHeader("accessToken", "11111111");  // 添加Header
        req.addQuery("username", "foo");  // 添加URL的Query参数
        return true;  // 继续执行请求返回true
    }
    /**
     * 该方法在请求成功响应时被调用
     */
    @Override
    public void onSuccess(T data, ForestRequest req, ForestResponse res) {
        log.info("invoke Simple onSuccess");
        // 执行成功接收响应后处理的代码
        int status = res.getStatusCode(); // 获取请求响应状态码
        String content = res.getContent(); // 获取请求的响应内容
        String result = (String)data;  // data参数是方法返回类型对应的返回数据结果,注意需要视情况修改对应的类型否则有可能出现类转型异常
        result = res.getResult(); // getResult()也可以获取返回的数据结果
        response.setResult("修改后的结果: " + result);  // 可以修改请求响应的返回数据结果
        
        // 使用getAttributeAsString取出属性,这里只能取到与该Forest请求对象,以及该拦截器绑定的属性
        String attrValue1 = getAttributeAsString(req, "A1");
    }
    /**
     * 该方法在请求发送失败时被调用
     */
    @Override
    public void onError(ForestRuntimeException ex, ForestRequest req, ForestResponse res) {
        log.info("invoke Simple onError");
        // 执行发送请求失败后处理的代码
        int status = res.getStatusCode(); // 获取请求响应状态码
        String content = res.getContent(); // 获取请求的响应内容
        String result = res.getResult(); // 获取方法返回类型对应的返回数据结果
    }
    /**
     * 该方法在请求发送之后被调用
     */
    @Override
    public void afterExecute(ForestRequest req, ForestResponse res) {
        log.info("invoke Simple afterExecute");
        // 执行在发送请求之后处理的代码
        int status = res.getStatusCode(); // 获取请求响应状态码
        String content = res.getContent(); // 获取请求的响应内容
        String result = res.getResult(); // 获取方法返回类型对应的最终数据结果
    }
}
 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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
Interceptor接口带有一个泛型参数,其表示的是请求响应后返回的数据类型。
Interceptor<String>即代表返回的数据类型为 String。
在拦截器的方法参数中基本都有 ForestRequest 类对象,即Forest请求对象,Forest的绝大部分操作都是围绕请求对象所作的工作。
文档导航
要详细了解 Forest 请求对象如何使用,请参见《请求对象》
# 拦截器与 Spring 集成
若我要在拦截器中注入 Spring 的 Bean 改如何做?
/**
 * 在拦截器的类上加上@Component注解,并保证它能被Spring扫描到
 */
@Component
public class SimpleInterceptor implements Interceptor<String> {
    // 如此便能直接注入Spring上下文中所有的Bean了
    @Resource
    private UserService userService;
    
    ... ...
}
 2
3
4
5
6
7
8
9
10
11
12
13
14
# 在拦截器中传递数据
在Forest中,拦截器是基于单例模式创建的,也就是说一个拦截器类最多只能对应一个拦截器实例。
那么以下这种通过共享变量的方式就可能造成错误:
public class SimpleInterceptor implements Interceptor<String> {
  
    private String name;
   
    @Override
    public boolean beforeExecute(ForestRequest req) {
        this.name = req.getQuery("name");
    }
    @Override
    public void onSuccess(String data, ForestRequest req, ForestResponse res) {
        System.out.println("name = " + name);
    }
}
 2
3
4
5
6
7
8
9
10
11
12
13
14
若有两个请求同时进入该拦截器(请求1 url=...?name=A1, 请求2 url=...?name=A2), 而最后当请求1进入onSuccess方法时,应该打印出 name = A2,却因为之前执行了请求2的beforeExecute方法,将类变量name的值改成了A2,
所以最终打印出来的是 name = A2 (其实应该是 name = A1),这明显是错误的。
那该如何做能在传递数据的同时避免这类问题呢?
方法也很简单,就是将您要传递的数据与请求对象绑定在一起,比如在 onSuccess 中调用req.getQuery方法。
System.out.println("name = " + forest.getQuery("name"));
 虽然这种方法能够解决并发问题,但有个明显的限制:如果要传递的数据不想出现在请求中的任何位置(包括URL、请求头、请求体),那就无能为力了。
这时候就要使用 ForestRequest 的扩展绑定数据的方法了。
# Attribute
在拦截器中使用addAttribute方法和getAttribute方法来添加和获取Attribute。
Attribute 是和请求以及所在拦截器绑定的属性值,这些属性值不能通过网络请求传递到远端服务器。
而且,在使用getAttribute方法时,只能获取在相同拦截器,以及相同请求中绑定的Attribute,这两个条件缺一不可。
public class SimpleInterceptor implements Interceptor<String> {
  
    @Override
    public void onInvokeMethod(ForestRequest req, ForestMethod method, Object[] args) {
        String methodName = method.getMethodName();
        addAttribute(req, "methodName", methodName); // 添加Attribute
        addAttribute(req, "num", (Integer) args[0]); // 添加Attribute
    }
    @Override
    public void onSuccess(String data, ForestRequest req, ForestResponse res) {
        Object value1 = getAttribute(req, "methodName");  // 获取名称为methodName的Attribute,不指定返回类型
        String value2 = getAttribute(req, "methodName", String.class);  // 获取名称为methodName的Attribute,并转换为指定的Class类型
        String value3 = getAttributeAsString(req, "methodName");  // 获取名称为methodName的Attribute,并转换为String类型
        Integer value4 = getAttributeAsInteger(req, "num");  // 获取名称为num的Attribute,并转换为Integer类型
    }
}
 2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Attachment
可以使用ForestRequest对象的addAttachment方法和getAttachment方法来添加和获取Attachment。
Attachment 是和请求绑定的附件属性值,这些值不能通过网络请求传递到远端服务器。
而且,在使用getAttachment方法时,只能获取在相同请求中绑定的Attachment,但不必是相同的拦截器。
public class SimpleInterceptor1 implements Interceptor<String> {
  
    @Override
    public void onInvokeMethod(ForestRequest req, ForestMethod method, Object[] args) {
        String methodName = method.getMethodName();
        req.addAttachment("methodName", methodName); // 添加Attachment
        req.addAttachment("num", (Integer) args[0]); // 添加Attachment
    }
    ... ...
}
/**
 * Attachment不依赖任何一个拦截器,可以跨拦截器传递数据
 */
public class SimpleInterceptor2 implements Interceptor<String> {
  
    @Override
    public void onSuccess(String data, ForestRequest req, ForestResponse res) {
        Object value1 = req.getAttachment("methodName");  // 获取名称为methodName的Attachment
        Object value2 = req.getAttachment("num");  // 获取名称为num的Attachment
    }
}
 2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Attribute与Attachment的区别
Attribute和Attachment都是能通过请求进行绑定的数据传递方式,但也有所不同。
| 绑定请求 | 绑定拦截器 | |
|---|---|---|
| Attribute | ✔ | ✔ | 
| Attachment | ✔ | ✘ | 
# 配置拦截器
Forest有三个地方可以添加拦截器:@Request、@BaseRequest、全局配置,这三个地方代表三个不同的作用域。
# @Request上的拦截器
若您想要指定的拦截器只作用在指定的请求上,只需要在该请求方法的@Request注解中设置interceptor属性即可。
public interface SimpleClient {
    @Request(
            url = "http://localhost:8080/hello/user?username=foo",
            headers = {"Accept:text/plain"},
            interceptor = SimpleInterceptor.class
    )
    String simple();
}
 2
3
4
5
6
7
8
9
10
@Request中拦截器可以配置多个:
    @Request(
            url = "http://localhost:8080/hello/user?username=foo",
            headers = {"Accept:text/plain"},
            interceptor = {SimpleInterceptor1.class, SimpleInterceptor2.class, ...}
    )
    String simple();
 2
3
4
5
6
友情提示
@Request上的拦截器只会拦截指定的请求
# @BaseRequest 上的拦截器
若您想使一个interface内的所有请求方法都指定某一个拦截器,可以在@BaseRequest的interceptor中设置
@BaseRequest(baseURL = "http://localhost:8080", interceptor = SimpleInterceptor.class)
public interface SimpleClient {
    @Request(url = "/hello/user1?username=foo" )
    String send1();
    @Request(url = "/hello/user2?username=foo" )
    String send2();
    @Request(url = "/hello/user3?username=foo" )
    String send3();
}
 2
3
4
5
6
7
8
9
10
11
12
13
14
如以上代码所示,SimpleClient接口中的send1、send2、send3方法都被会SimpleInterceptor拦截器拦截
@BaseRequest也如@Request中的interceptor属性一样,可以配1到多个拦截器,如代码所示:
@BaseRequest(
    baseURL = "http://localhost:8080", 
    interceptor = {SimpleInterceptor1.class, SimpleInterceptor2.class, ...})
public interface SimpleClient {
    // ... ...
}
 2
3
4
5
6
# 全局拦截器
若要配置能拦截项目范围所有Forest请求的拦截器也很简单,只要在全局配置中加上interceptors属性即可
forest:
  ...
  interceptors:                   # 可配置1到多个拦截器
     - com.your.site.client.SimpleInterceptor1
     - com.your.site.client.SimpleInterceptor2
     ...
 2
3
4
5
6