在铁科院做了一个关于医保报销的项目,在这个个系统中大量使用了下拉列表框,系统主要是给机关单位使用而且都是一些干部退休了啥的,年龄都比较大不愿意自己输入东西,因此界面上的很多值都是下拉列表框从数据字典表里面加载出来。
如此以来字典表的数据量变的越来越大,在一个界面上往往需要频繁的与字典表交互,觉的很影响性能于是我们增加了缓存,即为service层中的指定方法缓存功能,具体实现是利用Spring AOP+EHcache来做。
第一次执行某个方法的时候会去数据库里面查询,当第二次执行该方法时就会去从缓存里面查找,如有找到直接取值,找不到再去数据库里面查询。除此之外呢,我们还自定了tag标签,在界面上我们只需要引用一个写好的tag标签即可将一个下拉列表框加载到页面上,这样做也提高了下拉列表框的通用性,无论是前台还是后台将显示下拉列表的功能都抽象了出来,放到了一起,有益于代码的简洁和维护。
让我们看看是怎么样一步一步来实现这个功能的,涉及到了一些知识点有spring aop、encache缓存、自定义标签tag、tld文件等。
第一步:AOP为service层指定方法增加拦截,这里我拦截的是查询字典表的方法。
两种思路一种通过注解方式拦截,另一种通过配置文件方式但是我更倾向于使用配置文件,因为它灵活容易修改而且不需要改动代码,我把两种方式都试了一遍达到的效果是一样的。
配置文件如下;
1.通过配置文件<aop:config>来配置拦截
<span style="font-size:14px;"> <aop:config>
<aop:aspect id="cacheProcess" ref="cacheCheck">
<aop:after pointcut="execution(* com.zlwy.rcss.basedic.service.impl.BaseDicService.*(..))" method="processCache" />
</aop:aspect>
</aop:config>
<!-- 两种写法等价 -->
<aop:config>
<aop:aspect id="cacheProcess" ref="cacheCheck">
<aop:pointcut id="target" expression="execution(* com.zlwy.rcss.basedic.service.impl.BaseDicService.*(..))"/>
<aop:after method="processCache" pointcut-ref="target"/>
</aop:aspect>
</aop:config></span>
上面的两种写法是等价的,第一种将切点集合直接放到了<aop:after>里面,其实pointcut是<aop:after>标签的一个属性,下面的写法是把切点拿出来了,建议写第二种因为这种写好好理解,在写代码过程中易于阅读和方便理解也需要考虑。2.通过注解实现
切入的类:切面类
<span style="font-size:14px;">package com.zlwy.rcss.common;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.annotation.Around;
@Aspect
public class CacheInterceptor {
@Pointcut("execution(* com.zlwy.rcss.basedic.service.impl.BaseDicService.*(..))")
public void myMethod(){};
@Before("myMethod()")
public void processCache() throws Exception{
System.out.println("开始执行拦截器的processCache()方法");
}
@Around("myMethod()")
public Object doBasicProfiling() throws Throwable{
System.out.println("进入环绕通知");
System.out.println("退出方法");
return null;
}
}
</span>
在配置文件中需要开启切面注解如下
<span style="font-size:14px;"><aop:aspectj-autoproxy></aop:aspectj-autoproxy></span>
在切入的时候,遇到了一个错误查找额很多资料发现参数是固定好了的,不可以随意更改,只可以传入JoinPoint和ProceedingJoinPoint这两个接口作为参数或者不传入参数,如果是其他的方法将报找不到切入点的错误。
<span style="font-size:14px;">error at ::0 formal unbound in point</span>
第二步:为拦截的方法增加缓存ehcache,拦截的方法会执行下面的方法,转到缓存类的处理中并将调用对象上下文内容传入到缓存处理中。
<span style="font-size:14px;">public class CacheInterceptor {
public void processCache(ProceedingJoinPoint pjp) throws Throwable{
System.out.println("执行拦截器的processCache()方法----------开始");
CacheHander cacheHander=CacheHander.getCacheHander();
cacheHander.putResultToCache(pjp);
System.out.println("执行拦截器的processCache()方法----------结束");
}
}</span>
在处理被拦截的方法之前会先处理这个方法,然后调用缓存类CacheHander将查询出来的结果添加到缓存中,当第二次再调用这个方法的时候就会从缓存中取出数据,缓存中没有的话再从数据库里面查询。
<span style="font-size:14px;">package com.zlwy.rcss.common;
import java.io.Serializable;
import java.net.URL;
import org.aspectj.lang.ProceedingJoinPoint;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheException;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;
import net.sf.ehcache.ObjectExistsException;
public class CacheHander {
private CacheManager cacheManager;
//缓存变量
private Cache cache;
private static CacheHander cacheHander=new CacheHander();
//缓存名称
private final String cacheName="DATA_METHOD_CACHE";
/**
* 私有构造方法
*/
private CacheHander()
{
try {
//1.创建cachemanager
URL url=getClass().getResource("/customEHCache.xml");
System.out.println("encache.xml url="+url);
CacheManager cacheManager = CacheManager.create(url);
this.cacheManager=cacheManager;
cache=cacheManager.getCache(cacheName);
if(cache==null){
cache=new Cache("DATA_METHOD_CACHE", 10000, true, false, 600000, 300000);
cacheManager.addCache(cache);
}
System.out.println("cache.getSize()="+cache.getSize());
System.out.println("cache object="+cache);
} catch (CacheException e) {
e.printStackTrace();
}
}
/**
* 获取缓存类
* @returns
*/
public static CacheHander getCacheHander()
{
if (cacheHander==null) {
cacheHander=new CacheHander();
}
return cacheHander;
}
public Object putResultToCache(ProceedingJoinPoint pjp) throws Throwable
{
//原实体类名(包括包名)
String className=pjp.getTarget().getClass().getName();
//原方法名
String methodName=pjp.getSignature().getName();
//原方法实参列表
Object[] arguments=pjp.getArgs();
if (methodName.startsWith("get"))
{
String cacheKey=getCacheKey(className,methodName,arguments);
Element element=cache.get(cacheKey);
if (element==null) {
// 执行目标方法,并保存目标方法执行后的返回值
Object resuObject=pjp.proceed();
element=new Element(cacheKey, (Serializable)resuObject);
cache.put(element);
System.out.println("将查询结果放到缓存里面,缓存key="+cacheKey);
}else {
System.out.println("已经存在从缓存中取出来="+cacheKey);
}
return element.getValue();
}
return pjp.proceed();
}
/**
* @MethodName : getCacheKey
* @Description : 获得cache key的方法,cache key是Cache中一个Element的唯一标识 cache key包括
* 包名+类名+方法名+各个参数的具体指,如com.co.cache.service.UserServiceImpl.getAllUser
* @param targetName 类名
* @param methodName 方法名
* @param arguments 方法实参数组
* @return cachekey
*/
private String getCacheKey(String targetName, String methodName,
Object[] arguments) {
StringBuffer sb = new StringBuffer();
sb.append(targetName).append(".").append(methodName);
if ((arguments != null) && (arguments.length != 0)) {
for (int i = 0; i < arguments.length; i++) {
if(arguments[i] instanceof String[]){
String[] strArray = (String[])arguments[i];
sb.append(".");
for(String str : strArray){
sb.append(str);
}
}else{
sb.append(".").append(arguments[i]);
}
}
}
return sb.toString();
}
public Cache addCache(String cacheName) throws IllegalStateException, ObjectExistsException, CacheException
{
Cache cache=cacheManager.getCache(cacheName);
if (cache==null) {
cache=new Cache(cacheName,10000, true, false, 1000,100);
cacheManager.addCache(cache);
}
return cache;
}
public Cache getCache() {
return cache;
}
public void setCache(Cache cache) {
this.cache = cache;
}
public static void setCacheHander(CacheHander cacheHander) {
CacheHander.cacheHander = cacheHander;
}
}
</span>
利用AOP切入的关键是把切入前调用方法的上下文传入到切面类里面,比如调用该方法的对象、方法名、以及方法里面的执行参数等等,当我们缓存一个方法的查询结果的时候,需要给该结果指定一个唯一键值,方便我们从缓存中取出数据,这个键值相对于缓存的方法是唯一的,常常拿类的全名、方法名、传入的实参列表,三个参数当做缓存对象的key值。
通过这种切入式编程可以动态给程序增加新的功能,而不用动以前的代码,是一种不错的编程模式。
总结:
可以说AOP是面向对象编程OOP的补充和完善,OOP类设计好了之后结构是静态的、封闭的,任何需求的变化都可能对开发进度造成重要影响,试想一下OOP中引入了继承、封装、多太等特性来建立一种对象间的层次结构,在开发一个系统中对象会非常多,如果想为某些对象增加特殊功能则OOP无能为力,也可以增加进去但是一个一个增加很费事,OOP允许你定义从上到下的关系,但并不适合定义从左到右的关系。
例如日志功能,日志代码往往水平地散步在所有对象层次中,而与需要添加日志的类的核心功能毫无关系,再比如权限、事务等如果不实用AOP每一个方法都需要开启事务,在OOP中会导致大量重复性的代码,而不利用各个模块的重用。
而AOP技术则恰恰相反,它利用了一种称为“横切”的技术,将多个类公共行为封装到了一个可重用的模块内部,并将其命名为“ASpect”,即方面。
上面利用AOP为查询字典的方法增加了缓存功能是AOP技术的一种典型应用,它可以同其他一些技术结合实现为系统实现更多的新功能。