在铁科院做了一个关于医保报销的项目,在这个个系统中大量使用了下拉列表框,系统主要是给机关单位使用而且都是一些干部退休了啥的,年龄都比较大不愿意自己输入东西,因此界面上的很多值都是下拉列表框从数据字典表里面加载出来。
如此以来字典表的数据量变的越来越大,在一个界面上往往需要频繁的与字典表交互,觉的很影响性能于是我们增加了缓存,即为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技术的一种典型应用,它可以同其他一些技术结合实现为系统实现更多的新功能。