思考:在一个教务系统中,以下哪些是主要业务逻辑,哪些是次要业务逻辑?
AOP(面向方面/切面编程)是对OOP(面向对象编程)的补充,提供另一种关于程序结构的思维方式。OOP中模块化的关键单元是类,而AOP中模块化的单元是方面/切面(Aspect)。方面允许将横切多个类型和对象的关注点(如事务管理)模块化,在AOP的术语中,这种关注点通常叫做“横切关注点”。
AOP允许将次要业务逻辑相关的横切关注点模块化为方面,然后将方面切入到需要的目标对象中。
AOP实现将次要业务逻辑从主要业务逻辑中分离,从而降低程序耦合性和提高代码重用,让开发人员可以专注主要业务逻辑开发。
该思想的核心是:
实现AOP的过程中,我们会用到各种各样的组件和过程,我们使用不同的术语称呼不同的组件和过程,这些术语并非由Spring提供,而是在AOP中已经广泛使用的术语,Spring沿用了这些术语:
对于连接点和切入点,一言以蔽之:
通知类型:
如果不同方面中的两个或两个以上的通知应用到同一个连接点,除非指定了执行顺序,否则这多个通知的执行顺序是未知的,可以通过让方面类实现Ordered接口或使用@Order注解指定方面中通知的执行顺序。顺序数字越小,表示执行的优先级越高
定义切入点要定义两部分:
// 切入点表达式
@Pointcut("execution(* com.qdu.service.StudentService.*(..))")
// 切入点方法签名
public void pointcut1(){}
execution:动作关键字,描述切入点的行为动作,例如execution表示执行到指定切入点
public:访问修饰符,还可以是private等,可以省略
异常名:方法定义中抛出指定异常,可以省略
我们使用通配符描述切入点,主要的目的就是简化之前的配置,具体都有哪些通配符可以使用?
*:
单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现
execution(* com.qdu.service.impl.StudentServiceImpl.*(String))
execution(* com.qdu.service.StudentService.*(String,String))
..:
多个连续的任意符号,可以独立出现,常用于简化包名与参数的书写
execution(* com.qdu.service.StudentService.*(..))
execution(* com.qdu..*.*(..))
声明切入点的格式:一个切入点表达式+方法签名
方法名是引用这个切入点的名称,也就是切入点的名称
@Aspect
@Component
public class LogAspect {
@Pointcut("@within(com.qdu.aop.LogJoinPoint) || @annotation(com.qdu.aop.LogJoinPoint)")
public void pointcut() {
}
@Before("pointcut()")
public void beforeAdvice(JoinPoint point) {
System.out.println("---------前置通知,方法名称: "+point.getSignature().getName());
}
@AfterReturning("pointcut()")
public void AfterReturningAdvice(JoinPoint p) {
System.out.println("---------返回后通知,方法名称: "+p.getSignature().getName());
}
}
在定义切入点表达式中我们发现在@Pointcut中我们用的最多的是execution,上面的例子也用了@within、@annotation,这是标志符。定义切入点表达式时一般会用到以下几种标志符:
//1. execution标志符: 指定方法执行作为连接点,具体到方法
@Pointcut("execution(* com.qdu.service.*.*(..))")
//2. within标志符: 指定连接点所在的类型,具体到类型(实现类)
@Pointcut("within(com.qdu.service.impl.StudentServiceImpl)")
//3. target标志符: 指定目标对象对应的类型,具体到类型(接口或类)
@Pointcut("target(com.qdu.service.StudentService)")
//4. args标志符:args用于指定方法的参数,根据方法参数个数和类型去匹配构成切入点的连接点
//在切入点表达式中可以使用&&、||、!逻辑运算符
@Pointcut("args(String) || args(Integer,Integer)")
//5. bean标志符: 指定目标方法所在的bean的id或名称
@Pointcut("bean(mathServiceImpl) || bean(teacherServiceImpl)")
//6. @annotation标志符: 指定某个注解修饰的方法构成切入点,具体到指定注解修饰的方法
//指定注解的包名.注解名,说明该注解修饰的方法构成切入点
@Pointcut("@annotation(com.qdu.aop.LogJoinPoint)")
//7. @within标志符: 指定某个注解修饰的类型中的方法构成切入点,具体到指定注解修饰的类型
@within(com.qdu.aop.LogJoinPoint) //表示LogJoinPoint注解修饰的类型中的方法构成切入点
给出要应用切入点表达式的方法,试着写出切入点表达式:
以下是答案:
//1. 所有方法
@Pointcut("execution(* *(..))")
public void pointcut1() {
}
//2. 所有公开方法
@Pointcut("execution(public * *(..))")
public void pointcut2() {
}
//3. 所有以play开头的方法
@Pointcut("execution(* play*(..))")
public void pointcut3() {
}
//4. com.qdu.service.impl.StudentServiceImpl类型中的所有方法
@Pointcut("execution(* com.qdu.service.impl.StudentServiceImpl.*(..))")
public void pointcut4() {
}
//5. com.qdu.service.impl.StudentServiceImpl类型中带一个String类型参数的方法
@Pointcut("execution(* com.qdu.service.impl.StudentServiceImpl.*(String))")
public void pointcut5() {
}
//6. com.qdu.service.impl.StudentServiceImpl类型中带两个个String类型参数的方法
@Pointcut("execution(* com.qdu.service.impl.StudentServiceImpl.*(String, String))")
public void pointcut6() {
}
//7. com.qdu.service.StudentService类型中的所有方法
@Pointcut("execution(* com.qdu.service.StudentService.*(..))")
public void pointcut7() {
}
//8. com.qdu.service包下的所有类型中的所有方法
@Pointcut("execution(* com.qdu.service.*.*(..))") //注意第二个.*
public void pointcut8() {
}
//9. com.qdu.service包和其子包下所有类型中的所有方法
@Pointcut("execution(* com.qdu.service..*.*(..))") //注意第一个是..*
public void pointcut9() {
}
方面/切面(Aspect)是将次要业务逻辑/次要关注点/横切关注点模块化为方面,在这里我们将日志功能做成一个方面
在Spring的XML配置文件中使用
然后在com.qdu.aop包中创建一个LogAspect.class,这个类包含的是次要业务逻辑:日志功能。
@Aspect
public class LogAspect {}
如果要使用
首先方面对应的类应该注册为Spring管理的bean,才能将方面切入到需要的地方
在
也可以在SpringConfig配置类中使用@EnableAspectJAutoProxy注解,该注解的作用也是启用对@AspectJ、@Before等注解的支持。而且开启包扫描时不要忘记扫描次要业务逻辑的包(这里是com.qdu.aop)
@Configuration
@ComponentScan({"com.qdu.service","com.qdu.aop"})
@EnableAspectJAutoProxy
public class SpringConfig {
}
在LogAspect.java中,除了用@Aspect修饰这个包含次要逻辑的类,也不要忘记@Component
@Aspect
@Component
public class LogAspect {}
该方法我使用的不多,对此也并不是很理解,仅附上代码,日后待更
Spring的xml配置文件:
advisor1
advisor2
通知(Advice)即方面包含的操作,在spring代码中,通知对应的是方法
通知有5种类型:
LogAspect1.java:
public class LogAspect1 {
public void before1(JoinPoint point) {
System.out.println("..............前置通知1,目标方法:" + point.getSignature().getName());
}
public void before2(JoinPoint point) {
System.out.println("..............前置通知2");
}
}
returning属性用于指定一个参数名,这样可以在返回后通知对应的方法上添加一个该名称的参数,用于接收目标方法的返回值
LogAspect1.java:
public class LogAspect1 {
public void afterReturning(JoinPoint point, Object returnValue) {
System.out.println("..............返回后通知,返回值:" + returnValue);
}
}
throwing属性用于指定一个参数名,这样可以在抛出后通知对应的方法上添加一个该名称的参数,用于接收抛出的异常对象
LogAspect1.java:
public class LogAspect1 {
public void afterThrowing(JoinPoint point, Throwable ex) {
System.out.println("..............抛出后通知,异常消息:" + ex.getMessage());
}
}
LogAspect1.java:
public class LogAspect1 {
public void after(JoinPoint point) {
System.out.println("..............最终通知");
}
}
LogAspect1.java:
public class LogAspect1 {
public Object around(ProceedingJoinPoint point) throws Throwable {
System.out.println("..............环绕通知");
return point.proceed();
}
}
不同于XML配置,使用Java代码配置则是更多地使用Java配置类和注解
首先给出Java配置类,主要是开启包扫描及启用AspectJ风格
@Configuration
@ComponentScan({"com.qdu.service","com.qdu.aop"})
@EnableAspectJAutoProxy
public class SpringConfig {
}
接下来就是使用AspectJ的切入点表达式来指定应用通知的切入点,通过注解完成
@Before("execution(* com.qdu.service.StudentService.*(..))")
public void beforeAdvice1(JoinPoint point) {
System.out.println("~~~~~~~~~~~~~~~~~~~"+point.getSignature().getName()+"方法执行前");
}
这里添加了一个JoinPoint类型的参数,用于获取构成切入点的连接点的信息
这里我们给出一个实际的例子,此时我们有了一个com.qdu.service.MathService接口,并且有了该接口的实现类
我们对实现类中的add方法应用前置通知并获取方法中的一些信息:
@Before("execution(* com.qdu.service.MathService.*(..))")
public void beforeAdvice2(JoinPoint point) {
System.out.println("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
+point.getSignature().getName()+"方法调用前");
System.out.println("目标方法名:"+point.getSignature().getName());
System.out.println("目标方法详细信息:"+point.getSignature().toLongString());
//getTarget()可以获取目标对象,如果在通知中需要操作目标对象,可获取该对象操作
System.out.println("目标对象:"+point.getTarget().getClass());
System.out.print("目标方法的参数:");
//getArgs()用于获取目标方法传入的实际参数,以一个Object[]返回
Object[] args=point.getArgs();
for(Object arg:args) {
System.out.print(arg+" ");
}
System.out.println();
}
输出如下图所示:
@AfterReturning("execution(* com.qdu.service.*.*(..))")
public void afterReturningAdvice(JoinPoint p) {
System.out.println("..................."+p.getSignature().getName()+"方法执行返回后");
}
@AfterReturning注解的returning属性用于指定一个方法参数名,该名称对应的参数用于接收目标方法的返回值。如果希望表示任何类型的返回值,请使用Object类型
@Pointcut("execution(* com.qdu.service..*(..))")
public void pointcut1() {
}
@AfterReturning(value = "pointcut1()", returning = "returnValue")
public void afterReturningAdvice(JoinPoint point, Object returnValue) {
System.out.println("----------------------------------------"
+ point.getSignature().getName()
+ "方法调用返回后,返回值:"+returnValue);
}
调用后运行结果如下:
当然,若方法执行出现异常则不执行返回后通知
使用该类型的通知来收集异常信息。抛出后通知不会阻止异常的发生,只是在目标方法发生异常后做点事情。
一旦目标方法发生异常,会抛出异常对象,如果希望在抛出后通知中获取该异常对象,从而获取异常信息,可以通过throwing指定一个参数名,并在抛出后通知对应的方法上添加一个该名称的参数用于接收异常对象
如果指定异常类型是某个类型,则只有发生该类型异常的方法才会应用通知;如果希望能表示所有类型的异常,可以使用Exception或Throwable定义
@Aspect
@Component
public class LogAspect {
@Pointcut("execution(* com.qdu.service.MathService.*(..))")
public void pointcut1() {
}
@AfterThrowing(value = "pointcut1()",throwing="ex")
public void afterThrowingAdvice(JoinPoint point,Throwable ex) {
System.out.println("----------------------------------------"
+ point.getSignature().getName() + "方法抛出异常后,异常消息:"
+ex.getMessage());
}
}
运行一个会抛出异常的方法,如下图所示:
不管目标方法正常执行返回还是抛出异常都会执行的通知
@Aspect
@Component
public class LogAspect {
@Pointcut("execution(* com.qdu.service.MathService.*(..))")
public void pointcut1() {
}
@After("pointcut1()")
public void afterAdvice(JoinPoint point) {
System.out.println("----------------------------------------"
+ point.getSignature().getName()
+ "方法执行后,参数值:"
+point.getArgs()[0]+"和"+point.getArgs()[1]);
}
}
执行结果:
1. 环绕通知是包围一个方法的通知,它可以控制方法的执行
默认情况下,环绕通知会拦截目标方法的执行,也就是导致目标方法不会执行;如果希望能够获取连接点的信息和控制目标方法的执行,可以添加一个ProceedingJoinPoint类型的参数
@Aspect
@Component
public class LogAspect1 {
@Pointcut("execution(* com.qdu.service.*.playGames(..))")
public void pointcut1() {
}
@Around("pointcut1()")
public void aroundAdvice(ProceedingJoinPoint point) throws Throwable{
System.out.println("--------------------环绕通知,调用目标方法前");
//调用ProceedingJoinPoint的proceed()方法让目标方法执行
//这里获取连接点,也就是目标方法的参数,返回的是一个Object[]
//[0]表示获取方法的第一个参数,实际的参数是String类型,所以这里转成字符串
String gameName=point.getArgs()[0].toString();
//可以根据一个条件决定是否让目标方法执行
if(gameName.contains("绝地")||gameName.contains("求生")) {
System.out.println("打什么绝地求生,好好学习~~~");
} else {
point.proceed();
}
System.out.println("~~~~~~~~~~~~~~~~~~~~环绕通知,调用目标方法后");
}
}
运行结果如下:
2. 环绕通知不仅可以控制目标方法的执行,还可以控制目标方法的返回值
如果环绕通知返回类型为void,会导致有返回值的目标方法的返回值为null。可以在环绕通知中将目标方法的值返回,让目标方法的值得以正常返回
@Aspect
@Component
public class LogAspect2 {
@Pointcut("execution(* com.qdu.service.MathService.*(..))")
public void pointcut1() {
}
@Around("pointcut1()")
public Object aroundAdvice(ProceedingJoinPoint point) throws Throwable{
System.out.println("--------------------环绕通知,目标方法:"
+ point.getSignature().getName());
//proceed()方法调用会导致目标方法会调用,返回的值就是目标方法的返回值
Object returnValue=point.proceed();
//环绕通知这里返回的值就是最终返回的值
return returnValue;
}
}
运行结果如下:
看过以上两个例子,我们发现环绕通知可以将以上四种通知结合起来,也可以替代以上四种任何一种通知(但是尽量使用最合适的通知)
下面这个例子中,我们用环绕通知将以上四种通知集成到环绕通知中:
@Aspect
@Component
public class LogAspect {
@Pointcut("execution(* com.qdu.service.MathService.*(..))")
public void pointcut1() {
}
@Around("pointcut1()")
public Object aroundAdvice(ProceedingJoinPoint point) {
Object result=null;
try {
System.out.println("--------这是目标方法执行前执行的代码");
result=point.proceed();
System.out.println("~~~~~~~~这是目标方法正常执行返回后会执行的代码");
} catch (Throwable e) {
System.out.println("^^^^^^^^这是目标方法执行抛出异常后执行的代码");
} finally {
System.out.println("********这是目标方法不管是否出现异常,都会执行的代码");
}
return result;
}
}
运行10/2,发现该环绕通知实现了前置、返回后、后置通知的功能:
运行10/0,发现该环绕通知实现了前置、抛出后、后置通知的功能:
前面我们提到,如果多个同样的通知应用到同一个目标方法,执行顺序是未知的,但是可以通过使用@Order注解来控制执行顺序,@Order里的值越小表示顺序越靠前
假如我们有两个LogAspect,LogAspect1的Order值为2,有beforeAdvice1、beforeAdvice2、环绕通知、afterReturningAdvice1、afterReturningAdvice2;LogAspect2的Order值为1,有beforeAdvice3、beforeAdvice4;每个LogAspect都只有一个切入点,且该切入点运用到多个通知
@Aspect
@Component
@Order(2)
public class LogAspect1 {
@Pointcut("execution(* com.qdu.service.StudentService.*(..))")
public void pointcut1() {
}
@Before("pointcut1()")
public void doBefore1() {
System.out.println("..........beforeAdvice1..........");
}
@Before("pointcut1()")
public void doBefore2() {
System.out.println("..........beforeAdvice2..........");
}
@AfterReturning("pointcut1()")
public void doAfterReturning1() {
System.out.println("..........afterReturningAdvice1..........");
}
@AfterReturning("pointcut1()")
public void doAfterReturning2() {
System.out.println("..........afterReturningAdvice2..........");
}
@Around(value = "pointcut1()")
public void aroundAdvice(ProceedingJoinPoint point) throws Throwable {
System.out.println("..........环绕通知中调用目标方法前的代码..........");
point.proceed();
System.out.println("..........环绕通知中调用目标方法后的代码..........");
}
}
@Aspect
@Component
@Order(1) //值越小表示顺序越靠前
public class LogAspect2 {
@Pointcut("execution(* com.qdu.service.StudentService.*(..))")
public void pointcut1() {
}
@Before("pointcut1()")
public void doBefore3() {
System.out.println("..........beforeAdvice3..........");
}
@Before("pointcut1()")
public void doBefore4() {
System.out.println("..........beforeAdvice4..........");
}
}
LogAspect2的Order值小,所以LogAspect2中的通知应该优先执行。所以在4+1个前置通知中,LogAspect2中的beforeAdvice3、beforeAdvice4应优先执行,然后再执行LogAspect1中的
执行结果如下:
1 + 2.至少使用一次“切入点签名+切入点表达式”的写法定义一个该类中要用的切入点
定义一个方法,作为一个前置通知,包含目标方法执行前要执行的代码,通知应用到StudentService的所有方法
private static Logger logger = LoggerFactory.getLogger(LogAspect1.class);
@Pointcut("execution(* com.qdu.service.StudentService.*(..))")
public void pt1() {
}
@Before("pt1")
public void beforeAdvice(JoinPoint point) {
logger.debug("~~" + point.getSignature().getName() + "方法调用前");
}
3. 定义一个方法,作为一个返回后通知,包含目标方法执行成功返回后要执行的代码,通知应用到com.qdu.service包和子包内的所有类型的所有方法,但是排除divide2方法,并打印方法返回值
@AfterReturning(
value = "within(com.qdu.service..*) && !execution(* divide2(..))",
returning = "rv"
)
public void afterReturningAdvice(JoinPoint point, Object rv) {
logger.debug("**" + point.getSignature().getName() + "方法正常执行返回后,方法返回值" + rv);
}
4. 定义一个方法,作为一个抛出后通知,包含目标方法发生异常执行的方法,通知应用到MathService接口的divide开头的方法,并打印error级别的日志信息,要求打印异常消息
@AfterThrowing(
value = "execution(* com.qdu.service.MathService.divide*(..))",
throwing = "ex"
)
public void afterThrowingAdvice(JoinPoint point, Throwable ex) {
logger.debug("--" + point.getSignature().getName() + "方法发生异常后,异常消息:" + ex.getMessage());
}
5. 定义一个方法,作为一个环绕通知,应用到名为divide2方法上,用于集中处理异常,如果发生异常,打印error级别的日志信息和异常消息。请确保如果目标方法有返回值,返回值会正常返回
@Around("execution(* divide2(..))")
public Object aroundAdvice(ProceedingJoinPoint point) {
Object returnValue = null;
try {
returnValue = point.proceed();
} catch(Throwable e) {
logger.error("发生异常,目标方法:" + point.getSignature().getName() + ",异常信息:" + e.getMessage());
}
return returnValue
}
运行结果如下: