面向切面编程(AOP)是通过另一种思考方式来对面向对象编程(OOP)的补充。在抽象的结构中,OOP模块的基本单元是类,而AOP的基本单元是面。AOP的面能够跨越多个类型和对象来达成模块化。
下面是根据我的理解画的图:
这是一个简单的MVC结构,不同的模块之间根据类来分离。但是AOP的切面却可以跨越多个模块。图中的示例表示UserAOP跨越了整个Controller模块。当然他也可以同时跨越Model模块。这取决于AOP的实际业务需求。
AOP提供了一个不同的编程思路,不过springIoc并没有依赖AOP,对于Ioc来说,AOP可以提供支持但不是必须。
其中切入点可以和连接点合并,直接在通知中表示:
@Before("execution(com.dust.controller. . (..))")
/}
该写法等价于:
@Pointcut("execution(com.dust.controller.
.(..))")
public void point(){}
@Befor("point()")
public void before(){/
方法体 */}在AOP中,连接点与切入点的关联关系以及相应的判断规则是AOP的核心。切入点确定了AOP入口,但是具体要执行哪部分则由连接点决定。连接点是方法执行的入口。
@Before("execution(* com.dust.controller.*.*(..))") public void beforController() { //在Controller被调用前 } 复制代码
这里可以知道,对AOP来说,最小的单元是方法。AOP只能在方法和方法之间切入,而不能切入方法本身
@AfterReturning("execution(* com.dust.controller.*.*(..))") public void afterController() { //在Controller执行完之后 } 复制代码
对方法返回值的获取:
@AfterReturning( pointcut = "execution(* com.dust.controller.*.*(..))", returning = "retVal") public void afterController(Object retVal) { //对方法返回值的获取 System.out.println(retVal.toString()); } 复制代码
其中returning中的参数名称必须要和advice方法的参数名称相同,当方法执行返回时,返回值将作为相应的参数值传递给advice方法。如果方法没有返回值,则该参数为null。
@AfterThrowing("execution(* com.dust.controller.*.*(..))") public void afterController() { //在Controller抛出异常后执行 } 复制代码
也可以设置到抛出给定异常时才执行advice。
@AfterThrowing( pointcut = "execution(* com.dust.controller.*.*(..))", throwing = "ex") public void afterController(NullPointerException ex) { //抛出空指针异常时执行advice } 复制代码
@After("execution(* com.dust.controller.*.*(..))") public void afterController() { //最终执行通知 } 复制代码
所有通知方法都可以声明一个类行为org.aspectj.lang.JoinPoint的参数。
环绕通知声明的参数为ProceedingJoinPoint,因为需要执行ProceedingJoinPoint的proceed()方法。而其他的通知则不需要。
JoinPoint提供了很多有用的方法:
通常通知方法获取方法参数除了上述通过JoinPoint获取外还可以通过pointcut获取
@Pointcut("execution(* com.example.springdemo.controller.*.*(..)) && args(address, text, ..)") public void inController(String address, String text) {} @Before("inController(address, text)") public void beforController(String address, String text) { System.out.println("在执行控制器之前,获取参数{address:" + address + ",text:" + text + "}"); } 复制代码
args(address, text, ..)切入点表达式匹配切入点方法的参数,而后传入给advice方法。 这是要求所有和该切入点匹配的连接点都需要接收参数,还可以单单在连接点接收参数:
@Pointcut("execution(* com.example.springdemo.controller.*.*(..))") public void inController() {} @Before("inController() && args(address, text, ..)") public void beforController(String address, String text) { System.out.println("在执行控制器之前,获取参数{address:" + address + ",text:" + text + "}"); } 复制代码
其他参数:代理对象(this),目标对象(target)和注释(@within, @target, @annotation, @args)都可以以类似的方式绑定。
由于并发问题:死锁。可能导致业务执行失败。下次执行又有可能执行成功,因此对于这种操作不希望将重试交给用户来执行,这个可以交由系统来执行,这样对于用户来说他还是一次就执行成功了。
由于要尝试执行多次process(),因此使用@Around环绕通知
@Aspect @Configuration public class AOPRedo { //默认最大重试次数 private static final int DEFAULT_MAX_RETRIES = 2; private int maxRetries = DEFAULT_MAX_RETRIES; public void setMaxRetries(int maxRetries) { this.maxRetries = maxRetries; } @Around("execution(* com.example.springdemo.controller.RestAPIController.*(..))") public Object apiAroundController(ProceedingJoinPoint pjp) { int num = 0; Throwable throwable; do { num++; try { return pjp.proceed(); } catch (Throwable th) { System.out.println("尝试捕获"); throwable = th; } } while (num <= maxRetries); return null; } } 复制代码
其中重试次数可以交给配置文件,通过@PropertySource来导入配置信息。
@RestController @RequestMapping("api") public class RestAPIController { @Autowired EmailService emailService; private int count = 0; @RequestMapping("email") public String email(String address, String info) throws NullPointerException { if (count++ < 1) { throw new NullPointerException(); } return emailService.setEmail(address,info) + "执行次数:" + count; } } 复制代码
执行结果