前言
aop 是 aspect oriented program (面向切面)的编程的缩写。他是和面向对象编程相对的一个概念。在面向对象的编程中,我们倾向于采用封装、继承、多态等概念,将一个个的功能在对象中来实现。但是,我们在实际情况中也发现,会有另外一种需求就是一类功能在很多对象的很多方法中都有需要。例如有一些对数据库访问的方法有事务管理的需求,有很多方法中要求打印日志。按照面向对象的方式,那么这些相同的功能要在很多地方来实现或者在很多地方来调用。这就非常繁琐并且和这些和业务不相关的需求耦合太紧密了。所以后来就出现了面向切面的编程来解决这一类问题,并对面向对象的编程做了很好的补充
概念
要很好的理解面向切面的编程,先要理解 aop 的一些概念。在 java 中 aspectj 比较完整的实现了 aop 的功能,但是使用起来也比较复,所以这里主要是讨论 spring 的 aop 。spring aop 采用简单够用的原则,实现了 aop 的核心功能。下面先说说 aop 中的具体概念
- aspect:方面。一个可以切入多个类的关注点。这个关注点实现了我们前面说的具体的业务功能。例如打印日志,进行数据库的事务管理等。
- joint point:被切入点。是指具体要实现前面所说的例如打印日志,数据库事务管理的被切入的点。也就是通过 aop 将切面功能动态加入进去的程序位置。在 spring aop 里面这个指的都是某个方法
- pointcut:切点。用来指明如何通过规则匹配 joint point。这个规则是一个表达式。在 spring 中,默认使用的是 aspectj 的 pointcut 表达式语言
- advice:指明在一个切入点的不同位置上采取的动作。例如对于一个数据库访问事务管理来说,在进入方法后要开启事务,在方法结束前要提交事务,在发生错误的时候要回滚事务。这属于三个不同的 advice,要分别进行实现。advice 通常和具体的 pointcut 关联在一起。
- aop proxy:aop 代理。用来实现将 advice 功能动态加入到 pointcut 的方法。在 spring 的 aop 中采用动态代理和 cglib 代理的方式来实现。而 aspectj 则采用了特定编译器侵入字节码的方式来实现。
sprinboot aop 实现
前面我们已经用好几章讲述了 springboot 的基本使用。那么这里我们就用 springboot 和 aop 结合来实现一个输出所有 rest 接口输入参数和返回参数的日志的功能。
实现 rest 服务功能。
根据前面的文章,我们先建立一个 spingboot 的工程如下图所示
demo 工程
springboot 项目配置
我们对 springboot 项目配置如下
1
2
3
4
5
6
7
8
9
10
11
12
|
server: port: 3030 servlet: context-path: /aop-demo spring: jackson: date-format: yyyy-mm-dd hh:mm:ss serialization: indent-output: true logging: level: com.yanggch: debug |
其中 jackson 相关配置是为了将对象输出成 json 字符串后能够格式化输出
实现一个 rest 接口的 controller 类
在这里,我们实现两个 rest 接口。一个是返回 hello 信息。一个是根据输入返回登录信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
package com.yanggch.demo.aop.web; import com.yanggch.demo.aop.domain.loginentity; import com.yanggch.demo.aop.domain.securityentity; import org.springframework.web.bind.annotation.pathvariable; import org.springframework.web.bind.annotation.requestbody; import org.springframework.web.bind.annotation.requestmapping; import org.springframework.web.bind.annotation.requestmethod; import org.springframework.web.bind.annotation.restcontroller; import java.util.date; /** * 安全相关 rest 服务 * * @author : 杨高超 * @since : 2018-05-27 */ @restcontroller @requestmapping ( "/api/v1/security" ) public class securityapi { @requestmapping (value = "/login/{shopid}" , method = requestmethod.post) public securityentity login( @requestbody loginentity loginentity, @pathvariable long shopid) { securityentity securityentity = new securityentity(); securityentity.setshopid(shopid); securityentity.setaccount(loginentity.getaccount()); securityentity.setpwd(loginentity.getpwd()); securityentity.setlogintime( new date()); return securityentity; } @requestmapping (value = "/echo/{name}" , method = requestmethod.get) public string login( @pathvariable string name) { return "hello," + name; } } |
先在我们要通过 aop 功能将所有 rest 接口的输入参数和返回结果输出到日志中。
实现 web aop 功能。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
|
package com.yanggch.demo.aop.comment; import com.fasterxml.jackson.databind.objectmapper; import org.aspectj.lang.joinpoint; import org.aspectj.lang.annotation.afterreturning; import org.aspectj.lang.annotation.aspect; import org.aspectj.lang.annotation.before; import org.aspectj.lang.annotation.pointcut; import org.slf4j.logger; import org.slf4j.loggerfactory; import org.springframework.beans.factory.annotation.autowired; import org.springframework.stereotype.component; import org.springframework.web.multipart.multipartfile; import javax.servlet.http.httpservletrequest; import javax.servlet.http.httpservletresponse; /** * web 接口日志 * * @author : 杨高超 * @since : 2018-05-27 */ @aspect @component public class weblogaspect { private static logger log = loggerfactory.getlogger(weblogaspect. class ); private final objectmapper mapper; @autowired public weblogaspect(objectmapper mapper) { this .mapper = mapper; } @pointcut ( "@annotation(org.springframework.web.bind.annotation.requestmapping)" ) public void weblog() { } @before ( "weblog()" ) public void dobefore(joinpoint joinpoint) { for (object object : joinpoint.getargs()) { if ( object instanceof multipartfile || object instanceof httpservletrequest || object instanceof httpservletresponse ) { continue ; } try { if (log.isdebugenabled()) { log.debug( joinpoint.gettarget().getclass().getname() + "." + joinpoint.getsignature().getname() + " : request parameter : " + mapper.writevalueasstring(object) ); } } catch (exception e) { e.printstacktrace(); } } } @afterreturning (returning = "response" , pointcut = "weblog()" ) public void doafterreturning(object response) throws throwable { if (response != null ) { log.debug( "response parameter : " + mapper.writevalueasstring(response)); } } } |
这里有几个需要注意的地方,
- 需要在类上声明 org.aspectj.lang.annotation.aspect 注解。
- 需要通过方法上的 org.aspectj.lang.annotation.pointcut 注解声明一个 pointcut ,用来指明要在哪些方法切入。我们的 rest 接口都有 org.springframework.web.bind.annotation.requestmapping 注解,所以我们这里就用了 "@annotation(org.springframework.web.bind.annotation.requestmapping)" 表达式来指明。
- 通过 advice 相关注解来说明在切入方法的什么位置做什么事。这里用 org.aspectj.lang.annotation.before
- 这个实现是指明在所有具备 org.springframework.web.bind.annotation.requestmapping 注解的方法上,方法进入后打印入口参数。方法返回后,打印返回参数。
测试
在前台通过 postman 发起请求,后台日志输入结果如下
2018-05-27 19:58:42.941 debug 86072 --- [nio-3030-exec-4] c.yanggch.demo.aop.comment.weblogaspect : com.yanggch.demo.aop.web.securityapi.login : request parameter : {
"account" : "yanggch",
"pwd" : "123456"
}
2018-05-27 19:58:42.941 debug 86072 --- [nio-3030-exec-4] c.yanggch.demo.aop.comment.weblogaspect : com.yanggch.demo.aop.web.securityapi.login : request parameter : 2001
2018-05-27 19:58:42.942 debug 86072 --- [nio-3030-exec-4] c.yanggch.demo.aop.comment.weblogaspect : response parameter : {
"shopid" : 2001,
"account" : "yanggch",
"pwd" : "123456",
"logintime" : "2018-05-27 11:58:42"
}
2018-05-27 19:58:45.796 debug 86072 --- [nio-3030-exec-5] c.yanggch.demo.aop.comment.weblogaspect : com.yanggch.demo.aop.web.securityapi.echo : request parameter : "yanggch"
2018-05-27 19:58:45.796 debug 86072 --- [nio-3030-exec-5] c.yanggch.demo.aop.comment.weblogaspect : response parameter : "hello,yanggch"
由此可见,我们虽然没有在 rest 接口方法中写输出日志的代码,但是通过 aop 的方式可以自动的给各个 rest 入口方法中添加上输出入口参数和返回参数的代码并正确执行。
其他说明
前面提到了 advice 的类型和 pointcut 的 aop 表达式语言。具体参考如下。
advice 类型
- before advice 在方法执行前执行。
- after returning advice 在方法执行后返回一个结果后执行。
- after throwing advice 在方法执行过程中抛出异常的时候执行。
- around advice 在方法执行前后和抛出异常时执行,相当于综合了以上三种通知。
aop 表达式语言
1、方法参数匹配
@args()
2、方法描述匹配
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)throws-pattern?)
其中 returning type pattern,name pattern, and parameters pattern是必须的.
. ret-type-pattern:可以为表示任何返回值,全路径的类名等.
*. name-pattern:指定方法名, *代表所有
.set代表以set开头的所有方法.
. parameters pattern:指定方法参数(声明的类型),(..)代表所有参数,()代表一个参数
. (,string)代表第一个参数为任何值,第二个为string类型.
3、当前aop代理对象类型匹配
4、目标类匹配
@target()
@within()
5、标有此注解的方法匹配
@annotation()
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持服务器之家。
原文链接:https://www.jianshu.com/p/946c654f02c1