在任何一个项目中都不可或缺的存在两种bean,一种是实现系统核心功能的bean,我们称之为业务类,另外一种是与系统核心业务无关但同时又提供十分重要服务bean,我们称之为服务类。业务类的bean根据每个系统自身核心功能的不同可以有任意多个,但是服务类的种类在各个系统之间的差异却并不是很大。在系统中经常用到的服务有以下几种,权限服务,日志服务,缓存服务,事务服务以及预警服务等。在整个系统的不断进化过程中,服务类与业务类的关系也不断的发生着变化,由当初垂直模式变为横切模式,这也是编程思想不断演化过程。服务类与业务类本来就不应耦合在一起,否则不但会造成大量的代码冗余同时也难以对服务进行可控性变更。那么如何解决根据不同业务类自身要求为其提供不同服务的问题呢?spring aop给出了完美解决 方案。Spring解决这个难题的核心思想是将服务类与业务类统统交由spring容器管理,根据不同业务类的不同要求动态配置服务类,也即动态联编服务类与业务类实现两者的自由组合。至于业务类与服务类之间的关系演变可以用下图简单呈现一下:
这张图是最原始的业务-服务关系图,看到这连想都不敢想了,同样的服务重复出现在N多个业务中,想想也是醉了,如果哪天服务内容改变除了跳楼估计也没有别的选择了,还好Spring AOP的出现将这个让人头疼的问题。使用Spring 注解方式将切面类横切到各个服务类中就可以一绝解决代码冗余,难于维护的问题
本图在代码中的提现如下:
package com.test.util;
@Aspect
public class AuthorityService {
@Autowired
private LogManager logManager;
@Before("execution(* com.test.web.*.*(..))")
public void logAll(JoinPoint point) throws Throwable {
System.out.println("======authority-before======");
}
@After("execution(* com.test.web.*.*(..))")
public void after() {
System.out.println("=========authority-after=========");
}
// 方法执行的前后调用
@Around("execution(* com.test.web.*.*(..))")
public Object around(ProceedingJoinPoint point) throws Throwable {
System.out.println("======authority-around开始之前before======");
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder
.getRequestAttributes()).getRequest();
// 获得详细时间
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// Calendar ca = Calendar.getInstance();
// String operDate = df.format(ca.getTime());
Log sysLog = new Log();
// 开始时间
sysLog.setStartTime(df.format(new Date()));
// 获取ip地址
String ip = TCPIPUtil.getIpAddr(request);
String loginName;
String name;
String methodRemark = getMthodRemark(point);
String methodName = point.getSignature().getName();
String packages = point.getThis().getClass().getName();
if (packages.indexOf("$$EnhancerByCGLIB$$") > -1) { // 如果是CGLIB动态生成的类
try {
packages = packages.substring(0, packages.indexOf("$$"));
} catch (Exception ex) {
ex.printStackTrace();
}
}
Object object;
try {
// method_param = point.getArgs(); //获取方法参数
// String param=(String) point.proceed(point.getArgs());
object = point.proceed();
} catch (Exception e) {
// 异常处理记录日志..log.error(e);
throw e;
}
sysLog.setIp(ip);
sysLog.setClazz(packages);
sysLog.setMethod(methodName);
sysLog.setMessage(methodRemark);
// 结束时间
sysLog.setEndTime(df.format(new Date()));
// 返回结果
if (object == null) {
sysLog.setResult("无返回值");
} else {
sysLog.setResult(object.toString());
}
logManager.addLog(sysLog);
System.out.println("======authority-around开始之前after======");
return object;
}
@AfterReturning("execution(* com.test.web.*.*(..))")
public void x(){
System.out.println("-------authority-afterreturning-------");
}
// 获取方法的中文备注____用于记录用户的操作日志描述
public static String getMthodRemark(ProceedingJoinPoint joinPoint)
throws Exception {
String targetName = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
Object[] arguments = joinPoint.getArgs();
Class targetClass = Class.forName(targetName);
Method[] method = targetClass.getMethods();
String methode = "";
for (Method m : method) {
if (m.getName().equals(methodName)) {
Class[] tmpCs = m.getParameterTypes();
if (tmpCs.length == arguments.length) {
Logger methodCache = m.getAnnotation(Logger.class);
// 获得标记,为空时没有标记
if (methodCache != null) {
methode = methodCache.remark();
}
break;
}
}
}
return methode;
}
}
从上面代码可以看出该权限服务类切入到了com.test.web包下的所有类的所有方法上,同理我们可以再建立缓存服务类,日志服务类等分别切入到其他业务类中,现在看来这个令人头疼的问题似乎被spring基于@Aspect风格的aop解决掉了,但是我们仔细想一想问题真的解决了吗?大家都知道这样的切入方式粒度实在是太粗了,并不是相同包下的所有类的所有方法都需要这样的服务,举个很简单的例子,就拿缓存服务和日志服务来讲,日志服务是需要详细记录的它可以切入任何类的任何方法上,但是缓存服务却并不是这样,缓存服务只可能存在于类的获取数据的方法上,此时将缓存服务切入到类的修改,删除等方法上就显得非常奇葩了,再通俗的将就是任何一个服务都可能只存在于特定类的特定方法上,这个不确定性决定了只使用Spring @Aspect注解方式是难以解决下图问题的
此时我们需要怎样做呢?还好Spring同时提供了基于xml配置方式的aop,这种方式在相当大的程度上弥补了@Aspect不足,他可以根据不同的配置方式将不同的服务切入到不同类的不同方法上,这样细的粒度足以让我们实现各种服务对业务的动态组合。说道这可能会有人问既然spring提供了基于xml配置的aop,那我们只需要将不同的服务均配置在xml文件中不是同样可以实现业务与服务的动态组合吗?听上去似乎很有道理,果真这样的话似乎基于注解的aop似乎就可以被替代了。但是程序效率的问题又让我们不得不对xml配置方式和注解方式进行再次衡量。由于xml是在运行期动态联编确定切面的,解析xml的过程毫无疑问的会占用系统资源,如果将大量的aop配置配置在xml文件中将会得不偿失,而@Aspect方式支持编译期织入且不需要生成代理,这样就使得效率上会有优势。如此来看只要将xml方式与@Aspect方式混合使用,将粗粒度的服务(如日志和权限服务)使用@Aspect方式进行切入,对于细粒度的服务(如缓存服务)使用xml方式配置在spring中就可以完美解决以上问题了。下面是两种方式结合使用的实例,先来看配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.2.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.2.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd">
<!-- 注解扫描包 -->
<context:component-scan base-package="com.test" />
<!-- 开启注解 -->
<mvc:annotation-driven />
<!-- 静态资源(js/image)的访问 -->
<mvc:resources location="/js/" mapping="/js/**" />
<!-- 定义视图解析器 -->
<bean id="viewResolver"
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/"></property>
<property name="suffix" value=".jsp"></property>
</bean>
<!-- 启用Spring对基于@AspectJ aspects的配置支持 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
<bean id="logService" class="com.test.util.LogService"></bean>
<bean id="cacheService" class="com.test.util.CacheService"></bean>
<aop:config proxy-target-class="true">
<aop:aspect ref="cacheService">
<aop:pointcut id="log" expression="execution(* com.test.web.UserController.getAllUser(..))"/>
<aop:before pointcut-ref="log" method="logAll"/>
<aop:after pointcut-ref="log" method="after"/>
<aop:after-returning pointcut-ref="log" method="x"/>
<aop:around pointcut-ref="log" method="around"/>
</aop:aspect>
</aop:config>
</beans>
接下来是日志服务类:
package com.test.util;
@Aspect
public class LogService {
@Autowired
private LogManager logManager;
@Before("execution(* com.test.web.*.*(..))")
public void logAll(JoinPoint point) throws Throwable {
System.out.println("======log-before======");
}
@After("execution(* com.test.web.*.*(..))")
public void after() {
System.out.println("=========cache-after=========");
}
// 方法执行的前后调用
@Around("execution(* com.test.web.*.*(..))")
public Object around(ProceedingJoinPoint point) throws Throwable {
System.out.println("======log-around开始之前before======");
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder
.getRequestAttributes()).getRequest();
// 获得详细时间
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Log sysLog = new Log();
// 开始时间
sysLog.setStartTime(df.format(new Date()));
// 获取ip地址
String ip = TCPIPUtil.getIpAddr(request);
String loginName;
String name;
String methodRemark = getMthodRemark(point);
String methodName = point.getSignature().getName();
String packages = point.getThis().getClass().getName();
if (packages.indexOf("$$EnhancerByCGLIB$$") > -1) { // 如果是CGLIB动态生成的类
try {
packages = packages.substring(0, packages.indexOf("$$"));
} catch (Exception ex) {
ex.printStackTrace();
}
}
// String operatingcontent = "";
// Object[] method_param = null;
Object object;
try {
// method_param = point.getArgs(); //获取方法参数
// String param=(String) point.proceed(point.getArgs());
object = point.proceed();
} catch (Exception e) {
// 异常处理记录日志..log.error(e);
throw e;
}
sysLog.setIp(ip);
sysLog.setClazz(packages);
// 包名.+方法名
// packages + "." + methodName
sysLog.setMethod(methodName);
sysLog.setMessage(methodRemark);
// 结束时间
sysLog.setEndTime(df.format(new Date()));
// 返回结果
if (object == null) {
sysLog.setResult("无返回值");
} else {
sysLog.setResult(object.toString());
}
logManager.addLog(sysLog);
System.out.println("======log-around开始之前after======");
return object;
}
@AfterReturning("execution(* com.test.web.*.*(..))")
public void x(){
System.out.println("-------log-afterreturning-------");
}
// 获取方法的中文备注____用于记录用户的操作日志描述
public static String getMthodRemark(ProceedingJoinPoint joinPoint)
throws Exception {
String targetName = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
Object[] arguments = joinPoint.getArgs();
Class targetClass = Class.forName(targetName);
Method[] method = targetClass.getMethods();
String methode = "";
for (Method m : method) {
if (m.getName().equals(methodName)) {
Class[] tmpCs = m.getParameterTypes();
if (tmpCs.length == arguments.length) {
Logger methodCache = m.getAnnotation(Logger.class);
// 获得标记,为空时没有标记
if (methodCache != null) {
methode = methodCache.remark();
}
break;
}
}
}
return methode;
}
}
再接下来是缓存服务类;
package com.test.util;
public class LogService {
@Autowired
private CacheManager logManager;
public void pointcut() {
}
public void logAll(JoinPoint point) throws Throwable {
System.out.println("======cache-before======");
}
public void after() {
System.out.println("=========cache-after=========");
}
// 方法执行的前后调用
public Object around(ProceedingJoinPoint point) throws Throwable {
System.out.println("======cache-around开始之前before======");
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder
.getRequestAttributes()).getRequest();
// 获得详细时间
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// Calendar ca = Calendar.getInstance();
// String operDate = df.format(ca.getTime());
Log sysLog = new Log();
// 开始时间
sysLog.setStartTime(df.format(new Date()));
// 获取ip地址
String ip = TCPIPUtil.getIpAddr(request);
String loginName;
String name;
String methodRemark = getMthodRemark(point);
String methodName = point.getSignature().getName();
String packages = point.getThis().getClass().getName();
if (packages.indexOf("$$EnhancerByCGLIB$$") > -1) { // 如果是CGLIB动态生成的类
try {
packages = packages.substring(0, packages.indexOf("$$"));
} catch (Exception ex) {
ex.printStackTrace();
}
}
Object object;
try {
// method_param = point.getArgs(); //获取方法参数
// String param=(String) point.proceed(point.getArgs());
object = point.proceed();
} catch (Exception e) {
// 异常处理记录日志..log.error(e);
throw e;
}
sysLog.setIp(ip);
sysLog.setClazz(packages);
// 包名.+方法名
// packages + "." + methodName
sysLog.setMethod(methodName);
sysLog.setMessage(methodRemark);
// 结束时间
sysLog.setEndTime(df.format(new Date()));
// 返回结果
if (object == null) {
sysLog.setResult("无返回值");
} else {
sysLog.setResult(object.toString());
}
logManager.addLog(sysLog);
System.out.println("======cache-around开始之前after======");
return object;
}
public void x(){
System.out.println("-------cache-afterreturning-------");
}
}
日志服务类和缓存服务类只在具体处理过程中不一样,本文在日志服务的基础上稍加改动简化处理,大家可以在使用时根据自身情况进行缓存处理和日志处理。在具体运行过程中细心的朋友可能会发现在配置文件中不同服务的出现顺序决定了两个服务的执行顺序,也就是说在spring aop执行切入动作是通过装饰者模式来完成,采用类似栈的先进后出方式来具体执行服务这一点大家一定要注意啊。