在项目中,查询条件保持是经常使用到的,特别是管理后台。对于前台页面来说,通常为了访问的方便会使用get的方式进行表单提交,这样进行页面分享或者发送给好友时可以直接打开对于的页面。但是对于管理后台来说,地址栏上的一大串url参数是不允许的,不美观也不安全。
比如在用户查询页面,可以根据用户的年龄,姓名,昵称,等等参数进行查询,而且可能客户已经翻到了第n页上,此时点击某个用户详细,页面跳转到用户详细页面对用户信息进行编辑,编辑完成后点击保存,这时候需要返回到用户查询页面上,并且还得回到用户原来页面。那么可以使用如下的方式:

  • 弹出用户信息页面

这样的好处就是直接可以在页面上编辑,作为弹出层不影响之前查询的条件保持

  • 新开一个页面

这种方式通常不推荐使用,这样容易导致页面打开非常多,具体可以看客户需求

  • 在当前页面跳转

这种是使用最多的一种方式,因为在管理页面上通常来说是只在一个页面上操作的。但是多级页面跳转后查询条件的保持就是问题了。

对于前两种方式都比较简单,这里不多赘述了。本文将重点分析第三种需求的实现方式。

保持条件的方法

这里说说可行的几种方法:

  • 将查询页面中的所有参数带到后续所有页面中

这是最简单的方法,也是最累的方法,如果条件少跳转层级少这种方式是可以使用的,但是如果查询条件一多(通常管理页面查询条件是不少的)或者页面跳转多了,这种方式就呵呵了。

  • 保存到cookie中

作为参数少,且安全性不高的数据可以保存到cookie中,而且还必须管理好cookie的生命周期,其他用户登录时不能获取到之前用户的cookie信息。而且保存的信息不宜多。

  • 将参数缓到后端,等到返回查询页面时再从缓存中获取

比较推荐这种方式,将信息保存到后端后,生成一个缓存key,后面的页面只要传递一个key值即可。下面详述下这种方式的实现。


参数缓到后端

Spring项目中就可以直接使用切面编程和自定义参数注解的方式来实现,在一览页面查询时,将查询出的信息根据SessionId进行缓存即可。
首先介绍下使用Aspectj的方式来实现。

  1. 自定义缓存注解@SearchCache,通过此注解的标注的参数都表示需要缓存起来
  2. 拦截所有Controller中带有SearchCache注解的方法,在方法执行前将信息缓存起来
  3. 在Controller执行前,判断是否有cacheToken参数,如果有的话表示从缓存中读取,将缓存中读取的值作为参数传递到Controller中。
  4. 当用户退出登录时,将缓存信息清理掉

以上步骤简要的说明了整个实现思路,下面来一步步具体实现:
首先添加SearchCache,此注解是作用在方法参数中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@Documented
public @interface SearchCache {
Class<? extends ISearchCache> cacheImpl() default SessionSearchCache.class;

Class<? extends KeyGenerator> keyGenerator() default UUIDKeyGenerator.class;

/**
* 请求key
*
* @return
*/
String value() default "cacheToken";
}

cacheImpl:指定缓存的实现类,默认使用的是SessionSearchCache作为缓存,可以在使用注解的时候自定义设定缓存实现类,自定义缓存实现类需要实现ISearchCache接口。

keyGenerator:缓存key生成策略,默认使用的是UUIDKeyGenerator即使用UUID的方式生成缓存key,开发者可以自定义key生成的方式。

value:缓存的key,指定请求参数中哪个字段作为缓存key,并且生成的key将保存到model中对应的key。

其次,拦截所有的Controller方法,本文中将拦截所有参数中添加了注解@SearchCache的方法。并根据参数中是否有cacheToken参数来判断是否需要从缓存中获取查询数据,并且会将缓存key放入model中,方便后续逻辑处理。

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
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
102
103
104
105
106
107
108
109
110
@Aspect()
@Order(Ordered.HIGHEST_PRECEDENCE)
public class SearchCacheAspect implements BeanFactoryAware {

private static Logger log = LoggerFactory.getLogger(SearchCacheAspect.class);

private BeanFactory beanFactory;

@Pointcut("execution(* *..*.*(.. , @com.cml.learn.cacheablesearch.annotation.SearchCache (*), ..))")
public void cacheAspect() {
}

@Around("cacheAspect()")
public Object cacheAdvice(ProceedingJoinPoint point) throws Throwable {

// HttpServletRequest request = retrieveParam(point,
// HttpServletRequest.class);
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();

// do nothing
if (null == request) {
log.warn("not found HttpServletRequest param!!!");
return point.proceed();
}

Object[] args = point.getArgs();

// 获取添加了注解的参数对象
ParamHolder<SearchCache> searchCachePutHolder = retrieveParamConfig(point, SearchCache.class);
if (null != searchCachePutHolder) {
String cacheTokenKey = searchCachePutHolder.anonTarget.value();
// 修改args参数值
ISearchCache searchCache = beanFactory.getBean(searchCachePutHolder.anonTarget.cacheImpl());
Assert.notNull(searchCache, "cannot found impl class !!!!");

String cacheKey = request.getParameter(cacheTokenKey);
// 获取缓存
if (null != cacheKey) {
args[searchCachePutHolder.paramIndex] = searchCache.get(cacheKey);
} else {
cacheKey = generateKey(searchCachePutHolder);
// 生成缓存数据
Object value = args[searchCachePutHolder.paramIndex];
searchCache.put(cacheKey, value);
}
Model model = retrieveParam(point, Model.class);
if (null != model) {
model.addAttribute(cacheTokenKey, cacheKey);
}
}

Object value = point.proceed(args);

return value;
}

private String generateKey(ParamHolder<SearchCache> searchCachePutHolder) {
KeyGenerator keyGenerator = beanFactory.getBean(searchCachePutHolder.anonTarget.keyGenerator());
return keyGenerator.generateKey();
}

private <T> T retrieveParam(ProceedingJoinPoint point, Class target) {
Object[] args = point.getArgs();
MethodSignature signature = (MethodSignature) point.getSignature();
Method objMethod = signature.getMethod();
Annotation[][] anon = objMethod.getParameterAnnotations();
Class[] paramTypes = objMethod.getParameterTypes();
for (int i = 0; i < paramTypes.length; i++) {
if (paramTypes[i].equals(target)) {
return (T) args[i];
}
}
return null;
}

@SuppressWarnings("unchecked")
private <T> ParamHolder<T> retrieveParamConfig(ProceedingJoinPoint point, Class<? extends Annotation> anonTarget) {
Object[] args = point.getArgs();
MethodSignature signature = (MethodSignature) point.getSignature();
Method objMethod = signature.getMethod();
Annotation[][] anon = objMethod.getParameterAnnotations();

for (int i = 0; i < anon.length; i++) {
Annotation[] an = anon[i];
for (Annotation ann : an) {
if (ann.annotationType() == anonTarget) {
ParamHolder<T> holder = new ParamHolder<>();
holder.anonTarget = (T) ann;
holder.paramIndex = i;
holder.argValue = args[i];
holder.paramType = objMethod.getParameterTypes()[i];
return holder;
}
}
}
return null;
}

static class ParamHolder<T> {
T anonTarget;
Object argValue;
int paramIndex;
Class paramType;
}

@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
}

最后添加自动配置功能,添加EnableSearchCacheAutoConfiguration,此类会自动配置缓存功能的共通信息,比如缓存key的生成,切面功能的添加,以及Session的监听。

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
@Configuration
public class EnableSearchCacheAutoConfiguration {

@Bean
public UUIDKeyGenerator uuidKeyGenerate() {
return new UUIDKeyGenerator();
}

/**
* 注册session开关监听
*
* @param cache
* @return
*/
@Bean
public ServletListenerRegistrationBean<SessionCacheListener> sessionListener(SessionSearchCache cache) {
ServletListenerRegistrationBean<SessionCacheListener> listenerRegistration = new ServletListenerRegistrationBean<>();
listenerRegistration.setListener(new SessionCacheListener(cache));
listenerRegistration.setEnabled(true);
return listenerRegistration;
}

@Bean
public SearchCacheAspect searchCacheAspect() {
return new SearchCacheAspect();
}

@Bean
public SessionSearchCache sessionSearchCache() {
return new SessionSearchCache();
}

}

由于此功能是基于Aspectj实现的,所以在使用时候需要添加Aspectj功能,并且将自动配置类进行导入:

@Import(EnableSearchCacheAutoConfiguration.class)
@EnableAspectJAutoProxy

在Controller方法中添加@SearchCache()即可

1
2
3
4
5
6
@RequestMapping("/testPage")
public String testPage(Model model, @SearchCache() User u) {
model.addAttribute("key", "searchParam:" + u);
System.out.println("testPage==>");
return "dummy";
}

启动项目,访问地址
这里写图片描述

访问此页面后会将缓存的cacheToken返回,后续页面跳转中只需要带上cacheToken,再次返回查询页面的时候,系统会自动将cacheToken对应的数据获取出来。就上面的例子而言,在访问的时候即将cacheToken传入,age=1024就会自动从缓存中查询出来。
这里写图片描述

本文例子是通过SessionId,将用户的所有查询信息进行缓存,当session销毁时会自动将缓存中的信息清理。这样缓存请求参数的功能就轻地实现了。
文中的代码已上传至:https://github.com/cmlbeliever/cacheable-search
其中CacheableSearch工程为具体缓存实现功能lib,CacheableSearchProject为此项目的demo工程。至于使用方式,聪明的你应该知道。
虽然AOP的方式很使用,但是作为Spring的开发者来说,是不是有更好的方式来实现同样的功能?而且如果想要全局替换key的生成和缓存类,除了修改源码,还有什么更好的方式来实现?

下一篇文章将着重说明使用SpringMvc中自带的功能来实现完善此框架,待续…


使用了这么久的SpringBoot,但是还没有深入了解过注解的实现原理,想知道常用的注解实现原理么?想掌握各种Starter的实现原理么?可以看看我的课程:

http://gitbook.cn/gitchat/column/5a2fbea7626a7a2421b9a18c