SpringAOP在项目中的简单应用




SpringAOP在项目中的简单应用

一、前言

项目背景

由于公司的机房到期,需要做应用迁移(包括数据库),割接到云平台。但是又不能进行一次性割接,需要分三个阶段:第一阶段割接2个地市,第二阶段再割接9个地市,第三阶段做全盘割接。在第一、第二阶段是要保证云平台与现网平台的应用能够正常并行运行。所以,要保证两边数据访问的正常,需要对某些功能进行改造。比如,某个子功能在并行阶段是不做割接的,那么在做用户登录验证的时候,就需要访问两边的数据库来做验证。

技术背景

此次改造涉及到应用的改造,按理应该要修改原来的代码,才能保证在并行阶段的功能正常。这样子的话,就要维护两套代码,即云平台一套、现网一套。而且到了最后的割接的时候,还需要将代码进行还原,工作量非常大,且风险也高。后来想到用SpringAOP能够在不改动原代码的基础上加入一些附加的处理,而且只要配置好就行,功能也是支持热拔插的(将配置去掉即可)。

二、SpringAOP介绍

网上找了不少关于springAOP的相关资料,在这里做一个整合及小结,当然,只是初级层面的理解,没有深入研究其代码实现。

AOP概念

面向切面编程(也叫面向方面编程):Aspect Oriented Programming(AOP),是软件开发中的一个热点,也是Spring框架中的一个重要内容。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

AOP是OOP的延续。

主要的功能是:日志记录,性能统计,安全控制,事务处理,异常处理等等。

主要的意图是:将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。

可以通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能的一种技术。AOP实际是GoF设计模式的延续,设计模式孜孜不倦追求的是调用者和被调用者之间的解耦,提高代码的灵活性和可扩展性,AOP可以说也是这种目标的一种实现。

在Spring中提供了面向切面编程的丰富支持,允许通过分离应用的业务逻辑与系统级服务(例如审计(auditing)和事务(transaction)管理)进行内聚性的开发。应用对象只实现它们应该做的——完成业务逻辑——仅此而已。它们并不负责(甚至是意识)其它的系统级关注点,例如日志或事务支持。

AspectJ介绍

AspectJ是AOP的一个很悠久的实现,它能够和 Java 配合起来使用。

         这里介绍AspectJ 几个必须要了解的概念

·        切面(Aspect :官方的抽象定义为“一个关注点的模块化,这个关注点可能会横切多个对象”,在本例中,“切面”就是类TestAspect所关注的具体行为,例如,AServiceImpl.barA()的调用就是切面TestAspect所关注的行为之一。“切面”在ApplicationContext中来配置。

·        连接点(Joinpoint :程序执行过程中的某一行为,例如,AServiceImpl.barA()的调用或者BServiceImpl.barB(String_msg, int _type)抛出异常等行为。

·        通知(Advice :“切面”对于某个“连接点”所产生的动作,例如,TestAspect中对com.spring.service包下所有类的方法进行日志记录的动作就是一个Advice。其中,一个“切面”可以包含多个“Advice”,例如TestAspect

·        切入点(Pointcut :匹配连接点的断言,在AOP中通知和一个切入点表达式关联。例如,TestAspect中的所有通知所关注的连接点,都由切入点表达式execution(*com.spring.service.*.*(..))来决定

·        目标对象(Target Object :被一个或者多个切面所通知的对象。例如,AServcieImpl和BServiceImpl,当然在实际运行时,Spring AOP采用代理实现,实际AOP操作的是TargetObject的代理对象。

·        AOP代理(AOP Proxy 在Spring AOP中有两种代理方式,JDK动态代理和CGLIB代理。默认情况下,TargetObject实现了接口时,则采用JDK动态代理,例如,AServiceImpl;反之,采用CGLIB代理,例如,BServiceImpl。强制使用CGLIB代理需要将  的 proxy-target-class 属性设为true

   我们在使用该框架进行业务整改,主要的逻辑代码实就在于通知(Advice),常用有以下几种类型:

·        前置通知(Before advice :在某连接点(JoinPoint)之前执行的通知,但这个通知不能阻止连接点前的执行。ApplicationContext中在里面使用元素进行声明。例如,TestAspect中的doBefore方法

·        后通知(After advice :当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)。ApplicationContext中在里面使用元素进行声明。例如,TestAspect中的doAfter方法,所以AOPTest中调用BServiceImpl.barB抛出异常时,doAfter方法仍然执行

·        返回后通知(After return advice :在某连接点正常完成后执行的通知,不包括抛出异常的情况。ApplicationContext中在里面使用元素进行声明。

·        环绕通知(Around advice :包围一个连接点的通知,类似Web中Servlet规范中的Filter的doFilter方法。可以在方法的调用前后完成自定义的行为,也可以选择不执行。ApplicationContext中在里面使用元素进行声明。例如,TestAspect中的doAround方法。

抛出异常后通知( After throwing advice  :在方法抛出异常退出时执行的通知。 ApplicationContext中在里面使用元素进行声明。例如,TestAspect中的doThrowing方法。

二、SpringAOP介绍

AspectJ介绍

需要spring的核心包外,还需要aspectjrt.jar、aspectjweaver.ja、cglib-nodep.jar几个包。

         在ApplicationContext.xml中import进一个aop.xml配置,如下:




	
	aop配置

	
   
   


说明:这里主要是基于注解方式实现AOP,具体实现类看后面内容。这里的这要是防止抛java.lang.IllegalArgumentException异常,原因是AOP使用的动态代理可以针对接口,也可以针对类。java的动态代理只能针对接口。在用Spring的AOP时,默认动态代理是针对接口的,而我们是针对类的,所以要加上proxy-target-class="true"。

多数据源配置

由于项目需要,需要配置动态数据源,现网的应用有可能要访问云平台数据库的需要。

         Dao.xml配置如下(这里用到c3p0数据库连接池,数据库操作用springJDBC):




	
	
        
        
        
        
        
        
        
        
	
	
	
	
        
        
        
        
        
        
        
        
	
	
	
	
		
		
			
				
				
			
		
		
	
	
	
	
	

    
    
		
	
	
		
	
	
		
	
	
		
	


动态数据库类如下结构(这里借鉴了网上的实现):
DataSorceEntry.为接口:
public interface DataSourceEntry {

	// 云平台数据源标志
	public final static String YUN_SOURCE = "dataSource2";

	// 现网数据源标志
	public final static String CURR_SOURCE = "dataSource";
	/**
	 * 还原数据源
	 * 
	 */
	public void restore();
	
	/**
	 * 切换数据源
	 */

	public void switchSource();
	/**
	 * 设置数据源
	 * 
	 * @param dataSource
	 */
	public void set(String source);

	/**
	 * 获取数据源
	 * 
	 * @return String
	 */
	public String get();

	/**
	 * 清空数据源
	 */
	public void clear();
}

DataSourceEntryImpl为DataSorceEntry的实现类:
public class DataSourceEntryImpl implements DataSourceEntry {
	private final static ThreadLocal local = new ThreadLocal();
	
	public void clear() {
		local.remove();
	}

	public String get() {
		return local.get();
	}

	public void restore() {
		local.set(null); // 设置null数据源
	}

	public void set(String source) {
		local.set(source);
	}

	public void switchSource() {
		if (DataSourceEntry.CURR_SOURCE.equals(get())) {
			set(DataSourceEntry.YUN_SOURCE);
		}else {
			set(DataSourceEntry.CURR_SOURCE);
		}
	}
}

DynamicDataSource则为继承AbstractRoutingDataSource(springjdbc的多数据源路由类)类,该类以DataSorceEntry的实例作为数据源选择类,以注入方式实现:
public class DynamicDataSource extends AbstractRoutingDataSource {

	@Autowired
	private DataSourceEntry dataSourceEntry;

	@Override
	protected Object determineCurrentLookupKey() {
		return this.dataSourceEntry.get();
	}

	@Resource
	public void setDataSourceEntry(DataSourceEntry dataSourceEntry) {
		this.dataSourceEntry = dataSourceEntry;
	}
}

改造实例

这里以其中一个改造实进行说明,为了简单化,这里就举一个某个子系统的登录验证功能来说明,因为该子系统在并行期间是不做割接的,所以用户验证需访问两个平台的数据库。

DaYiAspest.java:

@Component
@Aspect
public class DaYiAspest{
	@Autowired
	private DataSourceEntry dataSourceEntry;//动态数据源


	//配置切入点集合
	@Pointcut("execution(* cn.qtone.xxt.parentnew.kwfd.controller.*.dayi(..)) " +
			"|| execution(* cn.qtone.xxt.schoolnew.kjck.controller.*.dayi(..))" +
			"|| execution(* cn.qtone.xxt.studentnew.kwfd.controller.*.dayi(..))")
	public void pointcuts(){}
	/**
	 * 单点登陆 (切入替换原方法)
	 * 
	 * @param request
	 * @param response
	 * @return
	 * @throws IOException
	 */
	@Around( value = "pointcuts()")
	public Object dayiLoginInit(ProceedingJoinPoint pjp) throws IOException {
		HttpServletRequest request = (HttpServletRequest) pjp.getArgs()[0];
		HttpServletResponse response = (HttpServletResponse) pjp.getArgs()[1];
		Object obj = null;
		//执行方法前操作
		obj = pjp.proceed();// 执行原操作
		//执行原方法后操作
		return obj;
	}
}

说明:@Pointcut可以定义多个切入点集合,也可以直接@Around( “execution表达式"),这里介绍一下execution表达式:

Spring AOP 用户可能会经常使用execution pointcut designator。执行表达式的格式如下:

 

execution(modifiers-pattern?ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)throws-pattern?)除了返回类型模式(上面代码片断中的ret-type-pattern),名字模式和参数模式以外,所有的部分都是可选的。返回类型模式决定了方法的返回类型必须依次匹配一个连接点。你会使用的最频繁的返回类型模式是 *,它代表了匹配任意的返回类型。一个全称限定的类型名将只会匹配返回给定类型的方法。名字模式匹配的是方法名。你可以使用 * 通配符作为所有或者部分命名模式。参数模式稍微有点复杂:() 匹配了一个不接受任何参数的方法,而 (..) 匹配了一个接受任意数量参数的方法(零或者更多)。模式 (*) 匹配了一个接受一个任何类型的参数的方法。模式 (*,String) 匹配了一个接受两个参数的方法,第一个可以是任意类型,第二个则必须是String类型。请参见AspectJ编程指南的 Language Semantics 部分。

 

下面给出一些常见切入点表达式的例子。

 

任意公共方法的执行:

execution(public **(..))任何一个以“set”开始的方法的执行:

execution(*set*(..))AccountService 接口的任意方法的执行:

execution(*com.xyz.service.AccountService.*(..))定义在service包里的任意方法的执行:

execution(*com.xyz.service.*.*(..))定义在service包或者子包里的任意方法的执行:

execution(*com.xyz.service..*.*(..))在service包里的任意连接点(在Spring AOP中只是方法执行):

within(com.xyz.service.*)在service包或者子包里的任意连接点(在Spring AOP中只是方法执行):

within(com.xyz.service..*)实现了AccountService 接口的代理对象的任意连接点(在Spring AOP中只是方法执行):

this(com.xyz.service.AccountService)'this'在binding form中用的更多:- 请常见以下讨论通知的章节中关于如何使得代理对象可以在通知体内访问到的部分。

实现了 AccountService 接口的目标对象的任意连接点(在Spring AOP中只是方法执行):

target(com.xyz.service.AccountService)'target'在binding form中用的更多:- 请常见以下讨论通知的章节中关于如何使得目标对象可以在通知体内访问到的部分。

任何一个只接受一个参数,且在运行时传入的参数实现了 Serializable 接口的连接点(在Spring AOP中只是方法执行)

args(java.io.Serializable)'args'在binding form中用的更多:- 请常见以下讨论通知的章节中关于如何使得方法参数可以在通知体内访问到的部分。

请注意在例子中给出的切入点不同于execution(* *(java.io.Serializable)): args只有在动态运行时候传入参数是可序列化的(Serializable)才匹配,而execution 在传入参数的签名声明的类型实现了 Serializable 接口时候匹配。

有一个 @Transactional 注解的目标对象中的任意连接点(在Spring AOP中只是方法执行)

@target(org.springframework.transaction.annotation.Transactional)'@target'也可以在binding form中使用:请常见以下讨论通知的章节中关于如何使得annotation对象可以在通知体内访问到的部分。

任何一个目标对象声明的类型有一个@Transactional 注解的连接点(在Spring AOP中只是方法执行)

@within(org.springframework.transaction.annotation.Transactional)'@within'也可以在bindingform中使用:- 请常见以下讨论通知的章节中关于如何使得annotation对象可以在通知体内访问到的部分。

任何一个执行的方法有一个@Transactional annotation的连接点(在Spring AOP中只是方法执行)

@annotation(org.springframework.transaction.annotation.Transactional)'@annotation'也可以在binding form中使用:- 请常见以下讨论通知的章节中关于如何使得annotation对象可以在通知体内访问到的部分。

任何一个接受一个参数,并且传入的参数在运行时的类型实现了 @Classified annotation的连接点(在Spring AOP中只是方法执行)

 

@args(com.xyz.security.Classified)'@args'也可以在bindingform中使用:- 请常见以下讨论通知的章节中关于如何使得annotation对象可以在通知体内访问到的部分。

 

还有这里我主要用了@Around的注解,其实还有好几种方式,它们的作用各不相同:

@Before:前置通知,在切点方法集合执行前,执行前置通知;

@After:后置通知,在切点方法集合执行后,执行后置通知;

@AfterReturning:后置通知,在切点方法集合执行后返回结果后,执行后置通知;

@Around :环绕通知(##环绕通知的方法中一定要有ProceedingJoinPoint参数,与Filter中的doFilter方法类似)

@AfterThrowing :异常通知,切点方法集合执行抛异常后执行处理;

具体实例可以看: http://blog.sina.com.cn/s/blog_7ffb8dd501014am6.html

在这里遇到一个问题:因为原方法是:public ModelAndView dayiLoginInit(HttpServletRequest request)只有一个参数,所以
上面的语句:
HttpServletResponse response = (HttpServletResponse) pjp.getArgs()[1];
是报数据越界的异常的,因为有些模是需要用到response 参数的,为了解决这个问题,我在网上找了解决方案,即配置过滤器,并运用线程变量ThreadLocal来实现:
过滤器:
import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
* @Description: 云平台割接过滤器
* @author 柯颖波
* @date 2014-4-1 下午03:04:34 
* @version v1.0
 */
public class GetContextFilter implements Filter {

	@Override
	public void destroy() {
	}

	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException,
			ServletException {
		SysContext.setRequest((HttpServletRequest) request);
		SysContext.setResponse((HttpServletResponse) response);
		chain.doFilter(request, response);
	}

	@Override
	public void init(FilterConfig filterConfig) throws ServletException {
		// TODO Auto-generated method stub
	}
}

SysContext 存储变量:

package cn.qtone.xxt.base.aop;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @Description: 系统请求上下文(主要存储request及response对象)
 * @author 柯颖波
 * @date 2014-4-1 下午02:49:58
 * @version v1.0
 */
public class SysContext {
	private static ThreadLocal requestLocal = new ThreadLocal();
	private static ThreadLocal responseLocal = new ThreadLocal();

	/**
	* @Description: 获取HttpServletRequest对象
	* @return    设定文件
	 */
	public static HttpServletRequest getRequest() {
		return (HttpServletRequest) requestLocal.get();
	}

	/**
	* @Description: 设置HttpServletRequest对象
	* @return    设定文件
	 */
	public static void setRequest(HttpServletRequest request) {
		requestLocal.set(request);
	}
	/**
	* @Description: 获取HttpServletResponse对象
	* @return    设定文件
	 */
	public static HttpServletResponse getResponse() {
		return (HttpServletResponse) responseLocal.get();
	}
	/**
	* @Description: 设置HttpServletResponse对象
	* @return    设定文件
	 */
	public static void setResponse(HttpServletResponse response) {
		responseLocal.set(response);
	}
	
	/**
	* @Description: 清除配置相关变量
	 */
	public static void clear() {
		requestLocal.remove();
		responseLocal.remove();
	}
}
那么,只要配好过滤器,那么上面的request,response对象可以这样获取:

HttpServletResponse response = SysContext.getResponse();
HttpServletRequest request =  SysContext.getRequest();

你可能感兴趣的:(J2EE学习)