在上篇博客中,我们了解了什么是AOP以及在Spring中如何使用AOP,本篇博客继续深入讲解下AOP的高级用法。

1. 声明带参数的切点

假设我们有一个接口CompactDisc和它的实现类BlankDisc:

package chapter04.soundsystem;

/**
* 光盘
*/
public interface CompactDisc {
void play(); void play(int songNumber);
}
package chapter04.soundsystem;

import java.util.List;

/**
* 空白光盘
*/
public class BlankDisc implements CompactDisc {
/**
* 唱片名称
*/
private String title; /**
* 艺术家
*/
private String artist; /**
* 唱片包含的歌曲集合
*/
private List<String> songs; public BlankDisc(String title, String artist, List<String> songs) {
this.title = title;
this.artist = artist;
this.songs = songs;
} @Override
public void play() {
System.out.println("Playing " + title + " by " + artist);
for (String song : songs) {
System.out.println("-Song:" + song);
}
} /**
* 播放某首歌曲
*
* @param songNumber
*/
@Override
public void play(int songNumber) {
System.out.println("Play Song:" + songs.get(songNumber - 1));
}
}

现在我们的需求是记录每首歌曲的播放次数,按照以往的做法,我们可能会修改BlankDisc类的逻辑,在播放每首歌曲的代码处增加记录播放次数的逻辑,但现在我们使用切面,在不修改BlankDisc类的基础上,实现相同的功能。

首先,新建切面SongCounter如下所示:

package chapter04.soundsystem;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut; import java.util.HashMap;
import java.util.Map; @Aspect
public class SongCounter {
private Map<Integer, Integer> songCounts = new HashMap<>(); /**
* 可重用的切点
*
* @param songNumber
*/
@Pointcut("execution(* chapter04.soundsystem.CompactDisc.play(int)) && args(songNumber)")
public void songPlayed(int songNumber) {
} @Before("songPlayed(songNumber)")
public void countSong(int songNumber) {
System.out.println("播放歌曲计数:" + songNumber);
int currentCount = getPlayCount(songNumber);
songCounts.put(songNumber, currentCount + 1);
} /**
* 获取歌曲播放次数
*
* @param songNumber
* @return
*/
public int getPlayCount(int songNumber) {
return songCounts.getOrDefault(songNumber, 0);
}
}

重点关注下切点表达式execution(* chapter04.soundsystem.CompactDisc.play(int)) && args(songNumber),其中int代表参数类型,songNumber代表参数名称。

新建配置类SongCounterConfig:

package chapter04.soundsystem;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy; import java.util.ArrayList;
import java.util.List; @Configuration
@EnableAspectJAutoProxy
public class SongCounterConfig {
@Bean
public CompactDisc yehuimei() {
List<String> songs = new ArrayList<>();
songs.add("东风破");
songs.add("以父之名");
songs.add("晴天");
songs.add("三年二班");
songs.add("你听得到"); BlankDisc blankDisc = new BlankDisc("叶惠美", "周杰伦", songs);
return blankDisc;
} @Bean
public SongCounter songCounter() {
return new SongCounter();
}
}

注意事项:

1)配置类要添加@EnableAspectJAutoProxy注解启用AspectJ自动代理。

2)切面SongCounter要被声明bean,否则切面不会生效。

最后,新建测试类SongCounterTest如下所示:

package chapter04.soundsystem;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import static org.junit.Assert.assertEquals; @RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SongCounterConfig.class)
public class SongCounterTest {
@Autowired
private CompactDisc compactDisc; @Autowired
private SongCounter songCounter; @Test
public void testSongCounter() {
compactDisc.play(1); compactDisc.play(2); compactDisc.play(3);
compactDisc.play(3);
compactDisc.play(3);
compactDisc.play(3); compactDisc.play(5);
compactDisc.play(5); assertEquals(1, songCounter.getPlayCount(1));
assertEquals(1, songCounter.getPlayCount(2)); assertEquals(4, songCounter.getPlayCount(3)); assertEquals(0, songCounter.getPlayCount(4)); assertEquals(2, songCounter.getPlayCount(5));
}
}

运行测试方法testSongCounter(),测试通过,输出结果如下所示:

播放歌曲计数:1

Play Song:东风破

播放歌曲计数:2

Play Song:以父之名

播放歌曲计数:3

Play Song:晴天

播放歌曲计数:3

Play Song:晴天

播放歌曲计数:3

Play Song:晴天

播放歌曲计数:3

Play Song:晴天

播放歌曲计数:5

Play Song:你听得到

播放歌曲计数:5

Play Song:你听得到

2. 限定匹配带有指定注解的连接点

在之前我们声明的切点中,切点表达式都是使用全限定类名和方法名匹配到某个具体的方法,但有时候我们需要匹配到使用某个注解的所有方法,此时就可以在切点表达式使用@annotation来实现,注意和之前在切点表达式中使用execution的区别。

为了更好的理解,我们还是通过一个具体的例子来讲解。

首先,定义一个注解Action:

package chapter04;

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Action {
String name();
}

然后定义2个使用@Action注解的方法:

package chapter04;

import org.springframework.stereotype.Service;

@Service
public class DemoAnnotationService {
@Action(name = "注解式拦截的add操作")
public void add() {
System.out.println("DemoAnnotationService.add()");
} @Action(name = "注解式拦截的plus操作")
public void plus() {
System.out.println("DemoAnnotationService.plus()");
}
}

接着定义切面LogAspect:

package chapter04;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component; import java.lang.reflect.Method; @Aspect
@Component
public class LogAspect {
@Pointcut("@annotation(chapter04.Action)")
public void annotationPointCut() {
} @After("annotationPointCut()")
public void after(JoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
Action action = method.getAnnotation(Action.class);
System.out.println("注解式拦截 " + action.name());
}
}

注意事项:

1)切面使用了@Component注解,以便Spring能自动扫描到并创建为bean,如果这里不添加该注解,也可以通过Java配置或者xml配置的方式将该切面声明为一个bean,否则切面不会生效。

2)@Pointcut("@annotation(chapter04.Action)"),这里我们在定义切点时使用了@annotation来指定某个注解,而不是之前使用execution来指定某些或某个方法。

我们之前使用的切面表达式是execution(* chapter04.concert.Performance.perform(..))是匹配到某个具体的方法,如果想匹配到某些方法,可以修改为如下格式:

execution(* chapter04.concert.Performance.*(..))

然后,定义配置类AopConfig:

package chapter04;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy; @Configuration
@ComponentScan
@EnableAspectJAutoProxy
public class AopConfig {
}

注意事项:配置类需要添加@EnableAspectJAutoProxy注解启用AspectJ自动代理,否则切面不会生效。

最后新建Main类,在其main()方法中添加如下测试代码:

package chapter04;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AopConfig.class); DemoAnnotationService demoAnnotationService = context.getBean(DemoAnnotationService.class); demoAnnotationService.add();
demoAnnotationService.plus(); context.close();
}
}

输出结果如下所示:

DemoAnnotationService.add()

注解式拦截 注解式拦截的add操作

DemoAnnotationService.plus()

注解式拦截 注解式拦截的plus操作

可以看到使用@Action注解的add()和plus()方法在执行完之后,都执行了切面中定义的after()方法。

如果再增加一个使用@Action注解的subtract()方法,执行完之后,也会执行切面中定义的after()方法。

3. 项目中的实际使用

在实际的使用中,切面很适合用来记录日志,既满足了记录日志的需求又让日志代码和实际的业务逻辑隔离开了,

下面看下具体的实现方法。

首先,声明一个访问日志的注解AccessLog:

package chapter04.log;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; /**
* 访问日志 注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AccessLog {
boolean recordLog() default true;
}

然后定义访问日志的切面AccessLogAspectJ:

package chapter04.log;

import com.alibaba.fastjson.JSON;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component; @Aspect
@Component
public class AccessLogAspectJ {
@Pointcut("@annotation(AccessLog)")
public void accessLog() { } @Around("accessLog()")
public void recordLog(ProceedingJoinPoint proceedingJoinPoint) {
try {
Object object = proceedingJoinPoint.proceed(); AccessLog accessLog = ((MethodSignature) proceedingJoinPoint.getSignature()).getMethod().getAnnotation(AccessLog.class); if (accessLog != null && accessLog.recordLog() && object != null) {
// 这里只是打印出来,一般实际使用时都是记录到公司的日志中心
System.out.println("方法名称:" + proceedingJoinPoint.getSignature().getName());
System.out.println("入参:" + JSON.toJSONString(proceedingJoinPoint.getArgs()));
System.out.println("出参:" + JSON.toJSONString(object));
}
} catch (Throwable throwable) {
// 这里可以记录异常日志到公司的日志中心
throwable.printStackTrace();
}
}
}

上面的代码需要在pom.xml中添加如下依赖:

<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.59</version>
</dependency>

然后定义配置类LogConfig:

package chapter04.log;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy; @Configuration
@ComponentScan
@EnableAspectJAutoProxy
public class LogConfig {
}

注意事项:不要忘记添加@EnableAspectJAutoProxy注解,否则切面不会生效。

然后,假设你的对外接口是下面这样的:

package chapter04.log;

import org.springframework.stereotype.Service;

@Service
public class MockService {
@AccessLog
public String mockMethodOne(int index) {
return index + "MockService.mockMethodOne";
} @AccessLog
public String mockMethodTwo(int index) {
return index + "MockService.mockMethodTwo";
}
}

因为要记录日志,所以每个方法都添加了@AccessLog注解。

最后新建Main类,在其main()方法中添加如下测试代码:

package chapter04.log;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(LogConfig.class); MockService mockService = context.getBean(MockService.class); mockService.mockMethodOne(1);
mockService.mockMethodTwo(2); context.close();
}
}

输出日志如下所示:

方法名称:mockMethodOne

入参:[1]

出参:"1MockService.mockMethodOne"

方法名称:mockMethodTwo

入参:[2]

出参:"2MockService.mockMethodTwo"

如果某个方法不需要记录日志,可以不添加@AccessLog注解:

public String mockMethodTwo(int index) {
return index + "MockService.mockMethodTwo";
}

也可以指定recordLog为false:

@AccessLog(recordLog = false)
public String mockMethodTwo(int index) {
return index + "MockService.mockMethodTwo";
}

这里只是举了个简单的记录日志的例子,大家也可以把切面应用到记录接口耗时等更多的场景。

4. 源码及参考

源码地址:https://github.com/zwwhnly/spring-action.git,欢迎下载。

Craig Walls 《Spring实战(第4版)》

汪云飞《Java EE开发的颠覆者:Spring Boot实战》

AOP(面向切面编程)_百度百科

原创不易,如果觉得文章能学到东西的话,欢迎点个赞、评个论、关个注,这是我坚持写作的最大动力。

如果有兴趣,欢迎添加我的微信:zwwhnly,等你来聊技术、职场、工作等话题(PS:我是一名奋斗在上海的程序员)。

Spring入门(十一):Spring AOP使用进阶的更多相关文章

  1. Spring入门IOC和AOP学习笔记

    Spring入门IOC和AOP学习笔记 概述 Spring框架的核心有两个: Spring容器作为超级大工厂,负责管理.创建所有的Java对象,这些Java对象被称为Bean. Spring容器管理容 ...

  2. Spring学习(十一)-----Spring使用@Required注解依赖检查

    Spring学习(九)-----Spring依赖检查 bean 配置文件用于确定的特定类型(基本,集合或对象)的所有属性被设置.在大多数情况下,你只需要确保特定属性已经设置但不是所有属性.. 对于这种 ...

  3. Spring课程 Spring入门篇 5-1 aop基本概念及特点

    概念: 1 什么是aop及实现方式 2 aop的基本概念 3 spring中的aop 1 什么是aop及实现方式 1.1 aop,面向切面编程,比如:唐僧取经需要经过81难,多一难少一难都不行.孙悟空 ...

  4. spring入门(六) spring mvc+mybatis

    1.引入依赖 <!-- https://mvnrepository.com/artifact/org.mybatis/mybatis --> <dependency> < ...

  5. spring入门(五) spring mvc+hibernate

    核心是让SessionFactory由Spring管理 1.引入依赖 <!-- https://mvnrepository.com/artifact/org.springframework/sp ...

  6. spring入门(四) spring mvc返回json结果

    前提:已搭建好环境 1.建立Controller package com.ice.controller; import com.ice.model.Person; import org.springf ...

  7. spring入门(八) spring mvc设置默认首页

    1.web.xml配置如下 <!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3// ...

  8. spring入门(七) spring mvc+mybatis+generator

    1.Mybatis-Generator下载 地址:https://github.com/mybatis/generator/releases 我使用的是 mybatis-generator-core- ...

  9. 【SSH进阶之路】一步步重构容器实现Spring框架——彻底封装,实现简单灵活的Spring框架(十一)

    文件夹      [SSH进阶之路]一步步重构容器实现Spring框架--从一个简单的容器開始(八)      [SSH进阶之路]一步步重构容器实现Spring框架--解决容器对组件的"侵入 ...

随机推荐

  1. 浅谈CMDB

    CMDB和运维自动化 一.运维 运维,指的是对已经搭建好的网络,软件,硬件进行维护.运维领域也是细分的,有硬件运维和软件运维 硬件运维主要包括对基础设施的运维,比如机房的设备,主机的硬盘,内存这些物理 ...

  2. ASP.NET Core MVC 之布局(Layout)

    1.什么是布局 布局通常包含常见的用户界面元素,例如应用头,导航或菜单元素和页脚,如图: 常见的HTML结构(例如脚本和样式表)也经常被应用程序中许多页面使用.所有这些共享元素都可以在布局中定义,然后 ...

  3. jquery实现最简单的下拉菜单

    <!doctype html> <html> <head> <meta charset="utf-8"> <title> ...

  4. 第二章: Java总结

    第二章笔记 什么是变量:变量代表一块内存区域,变量类型不一样,这一块内存的大小也不一样.#在编程语言里面,你可以通过定义变量,向内存里添加数据或者修改内存已有的数据.说白了,变量就是代表程序运行时存放 ...

  5. HTTP_5_通信数据转发程序:代理、网关、隧道

    HTTP通信时,除客户端和服务器之外,还有一些用于通信数据转发的应用程序,例如代理,网关,隧道.配合服务器工作. 代理 转发功能,客户端与服务器之间可有多个代理, 缓存代理:减少服务器压力,透明代理: ...

  6. java中dao层和service层的区别是什么

    dao层中已经有操作数据库的方法了,为什么还要service层去封装?有什么好处? tanghui12321 | 浏览 131990 次  我有更好的答案  推荐于2017-10-06 18:44:5 ...

  7. 戴尔PowerEdge T110 Ⅱ服务器U盘安装Windows Server 2019 DataCenter

    一. 下载准备 准备工作——下载Microsoft Windows Server 2019 官方简体中文激活版 (MSDN)原版iso镜像 准备工作——安装刻录软件UltraISO,单文件绿色版就够用 ...

  8. 10分钟了解一致性hash算法

    应用场景 当我们的数据表超过500万条或更多时,我们就会考虑到采用分库分表:当我们的系统使用了一台缓存服务器还是不能满足的时候,我们会使用多台缓存服务器,那我们如何去访问背后的库表或缓存服务器呢,我们 ...

  9. Docker:跨主机通信

    修改主机docker默认的虚拟网段,然后在各自主机上分别把对方的docker网段加入到路由表中,配合iptables即可实现docker容器夸主机通信.配置方法如下: 设有三台虚拟机 v1: 10.1 ...

  10. WinForm开发中通用附件管理控件设计开发参考

    1.引言 在WinForm开发中,文件附件的管理几乎在任何一个应用上都会存在,是一个非常通用集中的公共模块.我们日常记录会伴随着有图片.文档等附件形式来展现,如果为每个业务对象都做一个附件管理,或者每 ...