上一节中,因为反欺诈软件接口是通过aop的形式引入的,所以接下来介绍下aop的实现。其实再springboot中的实现aop是很简单的,仅仅需要知道几个注解就可以了。
准备工作:首先要引入springboot的基础包和aop对应的包。如下:
<dependencies>
<!-- 引入springboot的基础配置,这里的容器使用undertow,不适用tomcat -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<!-- 移除自带的tomcat -->>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
<version>1.4.0.RELEASE</version>
</dependency>
<!-- 引入undertow -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
<version>1.4.0.RELEASE</version>
</dependency>
<!--这个是aop需要引入jar包 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>1.4.0.RELEASE</version>
</dependency>
</dependencies>
上述的undertow和tomcat一样,这里我会使用,是因为公司在用undertow,听公司大佬说某些场景下,这个性能要好。
然后创建以下的包结构:
AopSpringApplication是springboot的启动类:代码很简单:
@SpringBootApplication
public class AopSpringApplication {
public static void main(String[] args) {
SpringApplication.run(AopSpringApplication.class, args);
}
}
1、@Before
对应的controller也如下:
@Controller
@RequestMapping("/v1")
public class AopController {
//这个是springboot自带的日志打印类,这个很好用,建议使用
//前段时间,公司日志的管理就是用logback管理的,提供按大小
//切割和按天回滚各种功能。
Logger logger = LoggerFactory.getLogger(this.getClass());
@RequestMapping("/before")
public ResponseEntity<?> aopBefore(@RequestParam String songName){
logger.info(String.format("这是输入参数%s", songName));
return new ResponseEntity(null,HttpStatus.OK);
}
}
对应的aop的前置通知如下:
package itouchtv.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
/**
* @author created by chenjianhui 2018/11/22
* 这两个不陌生吧,刚学spring的时候,总会讲,这两个是spring的
* 核心部分,一个是Aop和一个是Ioc,Aop表示切面,Ioc是依赖注入。
*/
@Aspect//声明为一个切面
@Component//注入到spring容器中
public class AopAspect {
private final static int FIRST_ARG = 0;//第一个参数
private final static int SENCOND_ARG = 1;//第二个参数
Logger logger = LoggerFactory.getLogger(this.getClass());
@Before("execution( * itouchtv.controller.v*.AopController.aopBefore(..))")
public void beforeTest(JoinPoint join) {
String songName = (String)join.getArgs()[FIRST_ARG];
logger.info(String.format("这是前置通知拦截的地方,拦截第一个参数第一个参数%s", songName));
}
}
然后在启动类中,右键run as - java application 就行了可以启动运用。在浏览器中输入:
http://localhost:8080/v1/before?songName=搁浅
就可以看到控制台中输出以下结果:
2018-11-22 22:38:56.501 INFO 7032 --- [ XNIO-2 task-1] itouchtv.aspect.AopAspect : 这是前置通知拦截的地方,拦截第一个参数第一个参数搁浅
2018-11-22 22:38:56.510 INFO 7032 --- [ XNIO-2 task-1] itouchtv.controller.v1.AopController : 这是输入参数搁浅
前面的是logback默认的日志格式。这可以看出前置通知起作用,也就是在调用实际接口的方法前,调用了切莫定义的那个方法。beforeTest(),同时还可以拿到实际调用函数的输入参数。
* itouchtv.controller.v*.AopController.aopBefore(..)
前面的一个*号代表任意返回值和访问类型,中间的*代表的是任意版本(公司里经常升级接口,需要个版本控制)。后面的两岸个点代表任意参数(注意不是两个任意参数,是任意参数,就是说,可以1个、2个。。。。任意个)
现在我在controller中加入一段代码异常代码:
@Controller
@RequestMapping("/v1")
public class AopController {
Logger logger = LoggerFactory.getLogger(this.getClass());
@RequestMapping("/before")
public ResponseEntity<?> aopBefore(@RequestParam String songName){
int i = 10;
i = i/0;
logger.info(String.format("这是输入参数%s", songName));
return new ResponseEntity(null,HttpStatus.OK);
}
}
然后继续执行,此时的输出结果为:
2018-11-22 22:45:51.879 INFO 14144 --- [ XNIO-2 task-1] itouchtv.aspect.AopAspect : 这是前置通知拦截的地方,拦截第一个参数第一个参数搁浅
2018-11-22 22:45:51.892 ERROR 14144 --- [ XNIO-2 task-1] io.undertow.request : UT005023: Exception handling request to /v1/before
尽管方法已经异常了,但是前置通知还是顺利执行了,没有任何影响,而且参数也可以拿到。
2、@AfterThrowing
异常通知主要是调用的方法出现异常的时候会触发。
实际调用的代码:
package itouchtv.controller.v1;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
@RequestMapping("/v1")
public class AopController {
Logger logger = LoggerFactory.getLogger(this.getClass());
@RequestMapping(value = "/aopAfterThrowing",method = RequestMethod.GET)
public ResponseEntity<?> aopAfterThrowing(){
int i = 10;
i = i/0;
return new ResponseEntity(null,HttpStatus.OK);
}
}
切面信息:
package itouchtv.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
/**
* @author created by chenjianhui 2018/11/22
* 这两个不陌生吧,刚学spring的时候,总会讲,这两个是spring的
* 核心部分,一个是Aop和一个是Ioc,Aop表示切面,Ioc是依赖注入。
*/
@Aspect//声明为一个切面
@Component//注入到spring容器中
public class AopAspect {
Logger logger = LoggerFactory.getLogger(this.getClass());
@AfterThrowing(value = "execution( * itouchtv.controller.v*.AopController.aopAfterThrowing(..))",throwing = "ex")
public void throwTest(Exception ex) {
logger.info(String.format("这是异常通知拦截的地方,出现的异常是:", ex));
}
}
然后在浏览器中访问:
http://localhost:8080/v1/aopAfterThrowing
便会在控制台打印以下信息:
2018-11-28 23:56:00.882 INFO 25708 --- [ XNIO-2 task-3] itouchtv.aspect.AopAspect : 这是异常通知拦截的地方,出现的异常是:
2018-11-28 23:56:00.884 ERROR 25708 --- [ XNIO-2 task-3] io.undertow.request : UT005023: Exception handling request to /v1/aopAfterThrowing
这里由于比较少介绍就顺便介绍下aop相关的几个概念:
上述标记有@Aspect这个注解的类就是一个切面,切面里面有连接点、通知等概念
连接点:excution对应的路径就是一个连接点:
* itouchtv.controller.v*.AopController.aopAfterThrowing(..)
第一个*代表任意公共类型和返回值,第二个*表示以v开头的字符,后面两个点表示任意参数。
通知在spring中,叫advice,有多种,这里介绍的是其中一种,即:@AfterThrowing,叫做后置异常通知。
上一节中的是前置通知@Before。(后面还有@AfterReturning,@After,@Around三个通知)
3、@AfterReturning
这个是后置返回通知,在发生异常的时候是不执行的。(奇怪,不是finally中吗?应该执行啊,不过我测试了,确实是不执行的)
具体代码如下:
package itouchtv.controller.v1;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
@RequestMapping("/v1")
public class AopController {
Logger logger = LoggerFactory.getLogger(this.getClass());
@RequestMapping("/aopAfterReturn")
public ResponseEntity<?> aopAfterReturn(@RequestParam String songName){
return new ResponseEntity(songName,HttpStatus.OK);
}
}
package itouchtv.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
/**
* @author created by chenjianhui 2018/11/22
* 这两个不陌生吧,刚学spring的时候,总会讲,这两个是spring的
* 核心部分,一个是Aop和一个是Ioc,Aop表示切面,Ioc是依赖注入。
*/
@Aspect//声明为一个切面
@Component//注入到spring容器中
public class AopAspect {
private final static int FIRST_ARG = 0;//第一个参数
private final static int SENCOND_ARG = 1;//第二个参数
Logger logger = LoggerFactory.getLogger(this.getClass());
@AfterReturning(value = "execution( * itouchtv.controller.v*.AopController.aopAfterReturn(..))",returning = "result")
public String afterReturnTest(JoinPoint join,Object result) {
String songName = (String)join.getArgs()[FIRST_ARG];
logger.info(String.format("这是后置返回通知拦截的地方,拦截第一个参数第一个参数%s", result));
ResponseEntity responseEntity = (ResponseEntity) result;
logger.info("这个是返回的状态"+responseEntity.getStatusCodeValue());
logger.info("这个是返回的结果"+(String) responseEntity.getBody());
return songName;//返回结果是没有用的,因为方法已经返回结果了才调用这个方法,所以这个方法可以拿到结果,却无法改变结果。
}
}
返回的结果:
2018-11-29 22:24:23.243 INFO 30644 --- [ XNIO-2 task-1] itouchtv.aspect.AopAspect : 这是后置返回通知拦截的地方,拦截第一个参数第一个参数<200 OK,搁浅,{}>
2018-11-29 22:24:23.243 INFO 30644 --- [ XNIO-2 task-1] itouchtv.aspect.AopAspect : 这个是返回的状态200
2018-11-29 22:24:23.243 INFO 30644 --- [ XNIO-2 task-1] itouchtv.aspect.AopAspect : 这个是返回的结果搁浅
看到我将result强制成了ResponseEntity了吗?
其实我是debug后才发现的,如下图:
同样的,看到我将body转成了String了吗,因为它本来就是String类型,也就是我controller返回的是什么就可以转成什么。
最后还要强调一下:方法发生异常时是无法拦截到的;还有就是无法改变返回的结果(网上有些说可以,我不太清楚,反正我是修改不成功,用@Around就可以修改)。
4、@After
这个是后置最终通知,这个即使是方法发生异常,也是可以拦截的,代码如下:
package itouchtv.controller.v1;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
@RequestMapping("/v1")
public class AopController {
Logger logger = LoggerFactory.getLogger(this.getClass());
@RequestMapping("/after")
public ResponseEntity<?> aopAfter(@RequestParam String songName){
int i = 10;
i = i/0;
logger.info(String.format("这是输入参数%s", songName));
return new ResponseEntity(null,HttpStatus.OK);
}
}
package itouchtv.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
/**
* @author created by chenjianhui 2018/11/22
* 这两个不陌生吧,刚学spring的时候,总会讲,这两个是spring的
* 核心部分,一个是Aop和一个是Ioc,Aop表示切面,Ioc是依赖注入。
*/
@Aspect//声明为一个切面
@Component//注入到spring容器中
public class AopAspect {
private final static int FIRST_ARG = 0;//第一个参数
private final static int SENCOND_ARG = 1;//第二个参数
Logger logger = LoggerFactory.getLogger(this.getClass());
@After("execution( * itouchtv.controller.v*.AopController.aopAfter(..))")
public void afterTest(JoinPoint join) {
String songName = (String)join.getArgs()[FIRST_ARG];
logger.info(String.format("这是后置通知拦截的地方,拦截第一个参数第一个参数%s", songName));
}
}
返回的结果为:
2018-11-29 22:37:22.741 INFO 30644 --- [ XNIO-2 task-3] itouchtv.aspect.AopAspect : 这是后置通知拦截的地方,拦截第一个参数第一个参数搁浅
2018-11-29 22:37:22.751 ERROR 30644 --- [ XNIO-2 task-3] io.undertow.request : UT005023: Exception handling request to /v1/after
这个同样是可以执行获取参数的操作的,方式都一样,这几个通知。
5、@Around
这个叫环绕通知,比较强大,可以修改返回的结果,如果要对结果做增强处理,建议使用。
这个通知的使用参数也不太一样,上面的获取参数都是使用JoinPoint连接点。这个是使用
package itouchtv.controller.v1;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
@RequestMapping("/v1")
public class AopController {
Logger logger = LoggerFactory.getLogger(this.getClass());
@RequestMapping(value = "/aopAround",method = RequestMethod.GET)
public ResponseEntity<?> aopAround(@RequestParam String songName){
return new ResponseEntity(songName,HttpStatus.OK);
}
}
package itouchtv.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
/**
* @author created by chenjianhui 2018/11/22
* 这两个不陌生吧,刚学spring的时候,总会讲,这两个是spring的
* 核心部分,一个是Aop和一个是Ioc,Aop表示切面,Ioc是依赖注入。
*/
@Aspect//声明为一个切面
@Component//注入到spring容器中
public class AopAspect {
private final static int FIRST_ARG = 0;//第一个参数
private final static int SENCOND_ARG = 1;//第二个参数
Logger logger = LoggerFactory.getLogger(this.getClass());
@Around(value = "execution( * itouchtv.controller.v*.AopController.aopAround(..))")
public Object aroundTest(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
//proceed()方法,其实会去执行目标方法,就是我这里控制器对对应的aopAround方法
//这里可能会出现异常的情况,其实这个结果跟动态代理是一样的。
Object result = null;
try{
logger.info("输入参数为:"+proceedingJoinPoint.getArgs()[0]+" 调用方法前,可以做其他事情,比如我要修改参数:");
Object[] args = proceedingJoinPoint.getArgs();
args[0] = "蒲公英的约定";
result = proceedingJoinPoint.proceed(args);
logger.info("返回的结果为:"+result+" 调用方法后,且不发生异常时,可以做其他事情,包括改变结果");
result = new ResponseEntity("夜的第七章", HttpStatus.OK);
}catch (Exception e){
logger.info("这里出现异常了",e);
}finally {
logger.info("不管异常与否,都可以做一些事情");
}
return result;
}
}
输出结果:
2018-11-29 23:20:34.085 INFO 23952 --- [ XNIO-2 task-1] itouchtv.aspect.AopAspect : 输入参数为:搁浅 调用方法前,可以做其他事情,比如我要修改参数:
2018-11-29 23:20:34.093 INFO 23952 --- [ XNIO-2 task-1] itouchtv.aspect.AopAspect : 返回的结果为:<200 OK,蒲公英的约定,{}> 调用方法后,且不发生异常时,可以做其他事情,包括改变结果
2018-11-29 23:20:34.093 INFO 23952 --- [ XNIO-2 task-1] itouchtv.aspect.AopAspect : 不管异常与否,都可以做一些事情
这里可以实现修改参数,比如一开始是搁浅输入进来,然后别修改成蒲公英的约定,proceed(args)完后,先返回<200 OK,蒲公英的约定,{}> ,所以可以知道,参数被修改了。然后修改结果,从浏览器返回的结果可以看出:
结果被修改了。Around够强大吧,你还可以进行异常的处理,finally的关闭,除了复杂点外,还可以跟动态代理结构对照学习。
实在是想不到有什么理由不学习环绕通知。
最后讲些抽象的概念:
切面:就是被@Aspect标记的类(这个类里面有切入点啊,连接点啊等等)
目标对象:实际要执行的那个方法的对象。看看我前面的动态代理,就是那个Object对象
代理对象:代理类的对象。就是加了功能,通过调用自己的方法来实现调用目标方法的对象。也就是动态代理中Proxy通过newInstance方法创建出来的对象。
连接点:对应被执行的目标方法(实际执行的方法,我这是controller中的方法)
切入点:连接点集合,其实就是pointcut,只不过我这里没有使用而已。看下代码:
@Pointcut("execution( * itouchtv.controller.v*.AopController.*(..))")
public void myPointCut(){}
@Before("myPointCut()")
public void test1(JoinPoint join) {
String songName = (String)join.getArgs()[FIRST_ARG];
logger.info(String.format("这是前置通知拦截的地方,拦截第一个参数第一个参数%s", songName));
}
@After("myPointCut()")
public void test2(JoinPoint join) {
String songName = (String)join.getArgs()[FIRST_ARG];
logger.info(String.format("这是后置通知拦截的地方,拦截第一个参数第一个参数%s", songName));
}
用@PointCut定义了一个切入点,然后就可以代表原来的那些连接点来使用,这里知道为什么是连接点的集合了吧。
当然,你也可以写两个连接点,不过PointCut这样可以省些代码罗,但它不是必须的。
织入:这个其实是目标对象生产代理对象的过程,也就是将需要添加的功能和实际的功能,一起放入到代理对象中,就是个织人的过程。
通知:就是上述的各种通知了,英文是Advice。