引子
有一个需求
1. 写一个计算器
2. 计算器接口: Interface—->ArithmeticCalculator
—->Calculator接口,里面有加减乘除四个方法。
3. 计算器接口实现类: 实现类—->ArithmeticCalculatorLogingImple—->实现了加减乘除四个方法
4. 日志: 在程序执行期间追踪正在发生的活动
接口代码实现
public interface ArithmeticCalculator {
int add(int i,int j);
int sub(int i,int j);
int mul(int i,int j);
int div(int i,int j);
}
包含日志功能的接口实现类代码
public class ArithmeticCalculatorLoggingImpl implements ArithmeticCalculator {
@Override
public int add(int i, int j) {
System.out.println("The method add begins with ["+i+","+j+"]");
int result = i + j;
System.out.println("The method add ends with "+ result);
return result;
}
@Override
public int sub(int i, int j) {
System.out.println("The method sub begins with ["+i+","+j+"]");
int result = i - j;
System.out.println("The method sub ends with "+ result);
return result;
}
@Override
public int mul(int i, int j) {
System.out.println("The method mul begins with ["+i+","+j+"]");
int result = i * j;
System.out.println("The method mul ends with "+ result);
return result;
}
@Override
public int div(int i, int j) {
System.out.println("The method div begins with ["+i+","+j+"]");
int result = i / j;
System.out.println("The method div ends with "+ result);
return result;
}
}
测试代码
public class Main {
public static void main(String[] args) {
ArithmeticCalculator arithmeticCalculator= null;
arithmeticCalculator=new ArithmeticCalculatorLoggingImpl();
int result = arithmeticCalculator.add(1, 2);
System.out.println("-->"+result);
result = arithmeticCalculator.div(4, 2);
System.out.println("-->"+result);
}
}
从上面的接口实现类的代码可以看到几点问题。
代码混乱:
越来越多的非业务需求(日志和验证等)
加入后,原有的业务方法急剧膨胀.
每个方法在处理核心逻辑的同时还必须
兼顾其他多个关注点。
代码分散:
以日志需求为例,只是为了满足这个单一需求
就不得不在多个模块(方法)里多次重复相同的日志代码。
如果日志需求发生变化,必须修改所有模块。
动态代理
使用动态代理解决上述问题
代理设计模式的原理:
使用一个代理将对象包装起来,
然后用该代理对象取代原始对象。
任何对原始对象的调用都要通过代理。
代理对象决定是否以及何时将方法调用转到原始对象上。
接口代码
public interface ArithmeticCalculator {
int add(int i,int j);
int sub(int i,int j);
int mul(int i,int j);
int div(int i,int j);
}
接口实现类代码
public class ArithmeticCalculatorImpl implements ArithmeticCalculator {
@Override
public int add(int i, int j) {
int result = i + j;
return result;
}
@Override
public int sub(int i, int j) {
int result = i - j;
return result;
}
@Override
public int mul(int i, int j) {
int result = i * j;
return result;
}
@Override
public int div(int i, int j) {
int result = i / j;
return result;
}
}
代理代码
ArithmeticCalculator target —-> ArithmeticCalculator proxy
在proxy中对原先定义的方法进行一定的”包装”。
public class ArithmeticCalculatorLoggingProxy {
//要代理的对象,代理类
private ArithmeticCalculator target;
//构造器
public ArithmeticCalculatorLoggingProxy(ArithmeticCalculator target){
this.target=target;
}
//代理
public ArithmeticCalculator getLoggingProxy(){
ArithmeticCalculator proxy=null;
//new出的对象JVM有默认的类加载器
//代理的对象要指定类加载器
ClassLoader loader=target.getClass().getClassLoader();
//代理对象的类型,即其中有哪些方法
Class [] interfaces = new Class[]{ArithmeticCalculator.class};
//当调用代理对象其中的方法时,该执行的代码
InvocationHandler h=new InvocationHandler() {
/**
* proxy:正在返回的代理对象.一般情况下,在invoke方法中都不使用该对象。
* method: 正在被调用的方法
* args: 调用方法时传入的参数
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
String methodName = method.getName();
//日志
System.out.println("The method " + methodName + " begins with" + Arrays.asList(args));
//执行方法
Object result=null;
try {
//前置通知
result=method.invoke(target, args);
//返回通知,可以访问到方法的返回值
} catch (Exception e) {
e.printStackTrace();
//异常通知,可以访问到方法出现的异常
}
//后置通知,因为方法可能会出异常,所以访问不到方法的返回值.
//日志
System.out.println("The method " + methodName + " ends with" + result);
return result;
}
};
//代理实例
proxy=(ArithmeticCalculator)Proxy.newProxyInstance(loader, interfaces, h);
return proxy;
}
}
测试代码
public class Main {
public static void main(String[] args) {
ArithmeticCalculator target = new ArithmeticCalculatorImpl();
ArithmeticCalculator proxy = new ArithmeticCalculatorLoggingProxy(target).getLoggingProxy();
int result = proxy.add(1, 2);
System.out.println("-->"+result);
result=proxy.div(4, 2);
System.out.println("-->"+result);
}
}
动态代理开发中可以直接用,但是不建议使用。
1.这样写有点麻烦
2.让一般的程序员搞动态代理要求有点高
AOP(面向切面编程)
一. AOP基础
AOP
(Aspct-Oriented Programming
,面向切面编程)
是一种新的方法论,是对传统OOP
(Object-Oriented Programming
,面向对象编程)的补充。
AOP
的主要编程对象是切面(aspect
),而切面是横切关注点模块化的一个特殊的对象。
在应用AOP
编程时,仍然需要定义公共功能,但可以明确的定义这个功能在哪里,
以什么方式应用,并且不必修改受影响的类.
这样一来横切关注点就被模块化到特殊的对象(切面)里。
AOP
的好处
每个事物逻辑位于一个位置,代码不分散,便于维护和升级
业务模块更简洁,只包含核心业务代码
AOP
术语
切面(Aspect
):横切关注点(跨越应用程序多个模块的功能)
被模块化的特殊对象
通知(Advice):切面必须要完成的工作。
目标(Target):被通知的对象
代理(Proxy):向目标对象应用通知之后创建的对象
连接点(Joinpoint):
程序执行的某个特定位置:如类某个方法调用前,调用后,方法抛出异常后等。
连接点由两个信息确定:方法表示的程序执行点相对点表示的方位。
例如ArithmethicCalculator#add()
方法执行前的连接点执行点为ArithmethicCalculator#add();
方位为该方法执行前的位置。
切点(pointcut):每个类都拥有多个连接点:例如ArithmethicCalculator的所有方法世纪上都是连接点。
即连接点是程序类中客观存在的事务。AOP通过切点定位到特定的连接点。
类比理解: 连接点相当于数据库中的记录,切点相当于查询条件。
切点和连接点不是一对一的关系,一个切点匹配多个连接点,
切点通过org.springframework.aop.Pointcut
接口进行描述,
它使用类和方法作为连接点的查询条件。
三对概念的个人理解
目标和代理:比方目标是接口,代理是包装过的接口切面和通知: 切面就是一个特殊的对象,里面的方法就是通知。它们是要对对象方法进行包装的方法。就等于说是共同功能。
连接点和切入点:
切入点就是用切入表达式来描述。来确定在目标接口方法当中那些方法上进行包装,这里把目标和切面结合起来了。连接点是是个对象,JoinPoint,用它来拿到目标方法的方法名和参数。
这个过程可以理解成:
接口就是目标, 切面就是模糊理解成代理(还不完全是)。
接口里头有很多方法,切面里头有很多通知。
在切面当中用切入点表达式定位到方法,用连接点获得方法的细节。然后进行包装的工作。
二. AspectJ
AspectJ
:Java
社区里最完整最流行的AOP
框架
在Spring 2.0
以上版本中,可以使用基于AspectJ
注解或基于XML
配置的AOP。
Spring
自己也有一个AOP
框架,不如AspectJ
。
在Spring中启用AspectJ注解支持
要在Spring应用中使用AspectJ注解,必须在classpath下包含AspectJ类库:
aopallicance.jar
aspectj.weaver.jar
spring-aspects.jar
将aop Schema添加到根元素中。
要在SpringIOC容器中启用AspectJ注解支持,只要在Bean配置文件中定义一个空的XML元素
当Spring IOC容器侦测到Bean配置文件中的元素时候,会
自动为与AspectJ切面匹配的Bean创建代理。
三. 用AspectJ注解声明切面
要在Spring
中声明AspectJ
切面,只需要在IOC
容器中将切面声明为Bean
实例。
当在Spring IOC
容器中初始化AspectJ
切面之后,
Spring IOC
容器就会为那些与AspectJ
切面相匹配的Bean
创建代理。
在AspectJ
注解中,切面只是一个带有@Aspect
注解的Java
类
通知是标注有某种注解的简单的Java
方法
AspectJ
支持5种类型的通知注解:
@Before
:前置通知,在方法执行之前执行。
@After
:后置通知,在方法执行之后执行。
@AfterRunning
:返回通知,在方法返回结果之后执行。
@AfterThrowing
:异常通知,在方法抛出异常之后
@Around
:环绕通知,围绕着方法执行
一个切面可以包括一个或者多个通知。
前置通知:在方法执行之前执行的通知。
前置通知使用@Before注解,并将切入点表达式的值作为注解值
后置通知: 后置通知是在连接点完成之后执行的,即连接点返回结果或抛出异常的时候,下面的后置通知记录了方法的终止。
在后置通知中还不能访问目标方法执行的结果。
目标方法执行的结果需要在返回通知当中访问。
返回通知: 目标方法正常执行,可以获取返回值的通知。
异常通知:目标方法发生了异常的时候,可以获取异常对象的通知。
引入通知(很少使用): 是一种特殊的通知类型,它通过为接口提供实现类,
允许动态地实现接口,就像对象已经在运行时扩展了实现类一样。
切入点表达式
利用方法签名编写AspectJ
切入点表达式
最典型的切入点表达式是根据方法的签名来匹配各种方法的:
最典型的切入点表达式时根据方法的签名来匹配各种方法:
execution * com.mozart.spring.ArithmeticCalculator.*(..)
匹配ArithmeticCalculator中声明的所有方法,
第一个*代表任意修饰符及任意返回值。
第二个*代表任意方法
..匹配任意数量的参数。
若目标类与接口与该切面在同一个包中,可以省略包名。execution public * ArithmeticCalculator.*(..)
匹配ArithmeticCalculator接口的所有公共方法。execution public double ArithmeticCalculator.*(..)
匹配ArithmeticCalculator中返回double类型数值的方法execution public double ArithmeticCalculator.*(double,..)
匹配第一个参数为double类型的方法,
..匹配任意数量任意类型的参数execution public double ArithmeticCalculator.*(double,double)
:
匹配参数类型为double,double类型的方法。
四. AspectJ使用步骤
1) 加入jar包
com.springsource.org.aopalliance-1.0.0.jar
com.springsource.org.aspectj.weaver-1.6.8.RELEASE.jar
spring-aop-4.0.0.RELEASE.jar
spring-aspects-4.0.0.RELEASE.jar
commons-logging-1.1.1.jar
spring-beans-4.0.0.RELEASE.jar
spring-context-4.0.0.RELEASE.jar
spring-core-4.0.0.RELEASE.jar
spring-expression-4.0.0.RELEASE.jar
2) 在配置文件中加入aop的命名空间
3) 基于注解的方式
①.在配置文件中加入如下配置:
②.把横切关注点的代码抽象到切面的类中.
i.切面首先是一个IOC中的Bean,即加入@Component注解
2.切面还需要加入@Aspect 注解
③.在类中声明通知
i.声明一个方法
ii.在方法前加入@Before注解
④.可以在通知方法中声明一个类型为JoinPoint的参数
然后就能访问链接细节.如方法名称和参数值。
五. AspectJ代码示例
目标接口代码
public interface ArithmeticCalculator {
int add(int i,int j);
int sub(int i,int j);
int mul(int i,int j);
int div(int i,int j);
}
目标接口实现类代码
@Component
public class ArithmeticCalculatorImpl implements ArithmeticCalculator {
@Override
public int add(int i, int j) {
int result = i + j;
return result;
}
@Override
public int sub(int i, int j) {
int result = i - j;
return result;
}
@Override
public int mul(int i, int j) {
int result = i * j;
return result;
}
@Override
public int div(int i, int j) {
int result = i / j;
return result;
}
}
切面代码
//把这个类声明为一个切面;需要把该类放入到IOC容器中;再声明为一个切面。
@Aspect
@Component
public class LoggingAspect {
//声明该方法是一个前置通知:在目标方法开始之前执行
@Before("execution(public int com.mozart.spring.aop.impl.ArithmeticCalculator.*(int, int))")
public void beforeMethod(JoinPoint joinPoint){
String methodName=joinPoint.getSignature().getName();
List<Object> args=Arrays.asList(joinPoint.getArgs());
System.out.println("The method " + methodName + " begins with " + args);
}
}
配置文件内容
<?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"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd">
<!-- 配置自动扫描的包 -->
<context:component-scan base-package="com.mozart.spring.aop.impl"></context:component-scan>
<!-- 使AspectJ 注解起作用: 自动为匹配的类生成代理对象-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
测试代码
public class Main {
public static void main(String[] args) {
//1. 创建Spring的IOC容器
ApplicationContext ctx=new ClassPathXmlApplicationContext("applicationContext.xml");
//2. 从IOC容器中获取bean的实例
ArithmeticCalculator arithmeticCalculator=ctx.getBean(ArithmeticCalculator.class);
//3. 使用bean
int result = arithmeticCalculator.add(3, 6);
System.out.println("result-->"+result);
result = arithmeticCalculator.div(12, 6);
System.out.println("result-->"+result);
}
}
六. AspectJ切面通知代码示例
这是对上面切面代码的一个补充。
//把这个类声明为一个切面;需要把该类放入到IOC容器中;再声明为一个切面。
@Aspect
@Component
public class LoggingAspect {
/**
*
* 在com.mozart.spring.aop.ArithmeticCalculator接口的实现类的每一个方法开始之前执行一段代码
*/
@Before("execution(* com.mozart.spring.aop.ArithmeticCalculator.*(..))")
public void beforeMethod(JoinPoint joinPoint){
String methodName=joinPoint.getSignature().getName();
Object [] args=joinPoint.getArgs();
System.out.println("The method " + methodName + " begins with " + Arrays.asList(args));
}
/**
* 后置通知,在方法执行之后执行的代码,无论该方法是否出现异常。
* @param joinPoint
*/
@After("execution(* com.mozart.spring.aop.ArithmeticCalculator.*(..))")
public void afterMethod(JoinPoint joinPoint){
String methodName=joinPoint.getSignature().getName();
System.out.println("The method " + methodName + " ends");
}
/**
* 返回通知,在方法正常结束以后执行的代码,返回通知是可以访问到方法的返回值的。
* @param joinPoint
*/
@AfterReturning(value="execution(* com.mozart.spring.aop.ArithmeticCalculator.*(..))",
returning="result")
public void afterReturning(JoinPoint joinPoint,Object result){
String methodName=joinPoint.getSignature().getName();
System.out.println("The method " + methodName + " ends with "+result);
}
/**
* 在目标方法出现异常时会执行代码
* 可以访问到异常对象,且可以指定在出现特定异常时执行通知,Exception--> NullPointerException
* @param joinPoint
* @param ex
*/
@AfterThrowing(value="execution(* com.mozart.spring.aop.ArithmeticCalculator.*(..))",
throwing="ex")
public void afterThrowing(JoinPoint joinPoint,Exception ex){
String methodName=joinPoint.getSignature().getName();
System.out.println("The method " + methodName + " occurs exception "+ ex);
}
/**
* 最强的,但是不是最常用的。
* 环绕通知需要携带ProceedingJoinPoint类型的参数.
* 环绕通知类似于动态代理的全过程: ProceedingJoinPoint 类型的参数可以决定是否执行目标方法.
* 且环绕通知必须有返回值。返回值必为目标方法的返回值
* @param pjd
*/
/*
@Around("execution(* com.mozart.spring.aop.ArithmeticCalculator.*(..))")
public Object aroundMethod(ProceedingJoinPoint pjd){
Object result=null;
String methodName=pjd.getSignature().getName();
try {
//前置通知
System.out.println("The method "+methodName+" begins with "+Arrays.asList(pjd.getArgs()));
//执行目标方法
result=pjd.proceed();
//返回通知
System.out.println("The method "+methodName+" ends with "+ result);
} catch (Throwable e) {
//异常通知
System.out.println("The method occurs exception: " + e);
throw new RuntimeException(e);
}
//后置 通知
System.out.println("The method "+methodName+" ends");
return result;
}
*/
}
七. AspectJ切面优先级
当目标有多个切面的时候,切面里头通知的包装顺序怎么确定呢?
可以使用@Order指定切面的通知的优先级。
值越小,优先级越高。
八. AspectJ切面中切入点表达式的复用
定义一个方法,用于声明切入点表达式,一般地,该方法中再不需要添入其他的代码
使用@Pointcut
来声明切入点表达式
后面的其他通知直接使用方法名来引用当前的切入点表达式
九. 基于注解配置AOP最终代码展示
日志切面代码
//把这个类声明为一个切面;需要把该类放入到IOC容器中;再声明为一个切面。
@Order(2)
@Aspect
@Component
public class LoggingAspect {
/**
* 定义一个方法,用于声明切入点表达式,一般地,该方法中再不需要添入其他的代码
* 使用@Pointcut来声明切入点表达式
* 后面的其他通知直接使用方法名来引用当前的切入点表达式
*/
@Pointcut("execution(* com.mozart.spring.aop.ArithmeticCalculator.*(..))")
public void declareJointPointExpression(){}
/**
*
* 在com.mozart.spring.aop.ArithmeticCalculator接口的实现类的每一个方法开始之前执行一段代码
*/
@Before("declareJointPointExpression()")
public void beforeMethod(JoinPoint joinPoint){
String methodName=joinPoint.getSignature().getName();
Object [] args=joinPoint.getArgs();
System.out.println("The method " + methodName + " begins with " + Arrays.asList(args));
}
/**
* 后置通知,在方法执行之后执行的代码,无论该方法是否出现异常。
* @param joinPoint
*/
@After("declareJointPointExpression()")
public void afterMethod(JoinPoint joinPoint){
String methodName=joinPoint.getSignature().getName();
System.out.println("The method " + methodName + " ends");
}
/**
* 返回通知,在方法正常结束以后执行的代码,返回通知是可以访问到方法的返回值的。
* @param joinPoint
*/
@AfterReturning(value="declareJointPointExpression()",
returning="result")
public void afterReturning(JoinPoint joinPoint,Object result){
String methodName=joinPoint.getSignature().getName();
System.out.println("The method " + methodName + " ends with "+result);
}
/**
* 在目标方法出现异常时会执行代码
* 可以访问到异常对象,且可以指定在出现特定异常时执行通知,Exception--> NullPointerException
* @param joinPoint
* @param ex
*/
@AfterThrowing(value="declareJointPointExpression()",
throwing="ex")
public void afterThrowing(JoinPoint joinPoint,Exception ex){
String methodName=joinPoint.getSignature().getName();
System.out.println("The method " + methodName + " occurs exception "+ ex);
}
/**
* 最强的,但是不是最常用的。
* 环绕通知需要携带ProceedingJoinPoint类型的参数.
* 环绕通知类似于动态代理的全过程: ProceedingJoinPoint 类型的参数可以决定是否执行目标方法.
* 且环绕通知必须有返回值。返回值必为目标方法的返回值
* @param pjd
*/
/*
@Around("declareJointPointExpression()")
public Object aroundMethod(ProceedingJoinPoint pjd){
Object result=null;
String methodName=pjd.getSignature().getName();
try {
//前置通知
System.out.println("The method "+methodName+" begins with "+Arrays.asList(pjd.getArgs()));
//执行目标方法
result=pjd.proceed();
//返回通知
System.out.println("The method "+methodName+" ends with "+ result);
} catch (Throwable e) {
//异常通知
System.out.println("The method occurs exception: " + e);
throw new RuntimeException(e);
}
//后置 通知
System.out.println("The method "+methodName+" ends");
return result;
}
*/
}
验证切面代码
@Order(1)
@Aspect
@Component
public class VlidationAspect {
@Before("LoggingAspect.declareJointPointExpression()")
public void vlidateArgs(JoinPoint joinPoint){
System.out.println("-->validate: "+Arrays.asList(joinPoint.getArgs()));
}
}
十. 基于xml配置AOP最终代码展示
<?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"
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-4.0.xsd">
<!-- 配置bean -->
<bean id="arithmeticCalculator"
class="com.mozart.spring.aop.xml.ArithmeticCalculatorImpl"></bean>
<!-- 配置切面的bean -->
<bean id="loggingAspect"
class="com.mozart.spring.aop.xml.LoggingAspect"></bean>
<bean id="vlidationAspect"
class="com.mozart.spring.aop.xml.VlidationAspect"></bean>
<!-- 配置aop -->
<aop:config>
<!-- 配置切点表达式 -->
<aop:pointcut expression="execution(* com.mozart.spring.aop.xml.ArithmeticCalculator.*(int,int))"
id="pointcut"/>
<!-- 配置切面及通知 -->
<aop:aspect ref="loggingAspect" order="2">
<aop:before method="beforeMethod" pointcut-ref="pointcut"/>
<aop:after method="afterMethod" pointcut-ref="pointcut"/>
<aop:after-throwing method="afterThrowing" pointcut-ref="pointcut" throwing="ex"/>
<aop:after-returning method="afterReturning" pointcut-ref="pointcut" returning="result"/>
<!--
<aop:around method="aroundMethod" pointcut-ref="pointcut"/>
-->
</aop:aspect>
<aop:aspect ref="vlidationAspect" order="1">
<aop:before method="vlidateArgs" pointcut-ref="pointcut"/>
</aop:aspect>
</aop:config>
</beans>