约定编程~Spring AOP
1. 约定编程
1.1 约定
我们先准备一下测试的环境
先写一个服务接口和服务实现
public interface HelloService {
void sayHello(String name);
}
public class HelloServiceImpl implements HelloService{
@Override
public void sayHello(String name) {
if (name == null || name.trim().equals("")){
throw new RuntimeException("the name is empty");
}
System.out.println("hello " + name +" !!!");
}
}
这个服务很简单,就是,名字不为null和名字不为空时打印hello + name 否则抛出异常
定义一个拦截器
/**
* 拦截器接口
*/
public interface Interceptor {
//事前方法
boolean before();
//事后方法
void after();
/**
* 取代原有的事件方法
* @param invocation --回调参数,可以通过它来回调原有事件的方法
* @return 原有事件返回的对象
* @throws InvocationTargetException
* @throws IllegalAccessException
*/
Object around(Invocation invocation) throws InvocationTargetException,IllegalAccessException;
//是否返回方法,事件没有发生异常的情况下调用
void afterReturning();
//事后异常方法,当事件发生异常后执行
void afterThrow();
//是否调用around方法取代原有方法
boolean useAround();
}
接口的定义也完成了,后面的工作就是把这些方法织入流程,这就需要约定了。
先给出around(Invocation invocation)的源码
public class Invocation {
private Object[] params;
private Method method;
private Object target;//被代理的对象
public Invocation(Object[] params, Method method, Object target) {
this.params = params;
this.method = method;
this.target = target;
}
public Object proceed() throws InvocationTargetException,IllegalAccessException{
return method.invoke(target,params);
}
/*get set ...*/
}
通过反射去执行被代理对象的方法
实现这个拦截器
public class MyInterceptor implements Interceptor{
@Override
public boolean before() {
System.out.println("before .... ");
return useAround();
}
@Override
public boolean useAround() {
return true;
}
@Override
public void after() {
System.out.println("after .... ");
}
@Override
public Object around(Invocation invocation) throws InvocationTargetException, IllegalAccessException {
System.out.println("around before.... ");
Object proceed = invocation.proceed();
System.out.println("around after.... ");
return proceed;
}
@Override
public void afterReturning() {
System.out.println("afterReturning.... ");
}
@Override
public void afterThrow() {
System.out.println("afterThrow.... ");
}
}
我们需要一个代理对象-ProxyBean来执行这些流程,ProxyBean怎么实现后面再说,实现之后我们需要他能执行以下流程
- 使用 proxy调用方法时会先执行拦截器的 before方法
- 如果拦截器的 useAround方法返回true,则执行拦截器的 around方法,而不调用 target,对象对应的方法, around方法的参数Invocation对象存在一个 proceed方法,它可以调用 target对象对应的方法;如果 useAround方法返回 false,则直接调用 target对象的事件方法
- 无论怎么样,在完成之前的事情后,都会执行拦截器的 after方法
- 在执行 around方法或者回调 target I的事件方法时,可能发生异常,也可能不发生异常。如果发生异常,就执行拦截器的 afterThrowing方法,否则就执行 afterReturning方法。
下图是整个约定流程
这边的服务和拦截器都已经定义好了,那么问题就是怎么把这些拦截器里面的方法织入约定的流程呢,也就是ProxyBean是怎么实现的?
1.2 ProxyBean的实现
JDK提供了一个可以创造代理对象的方法,方法如下
public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h) throws ...
-
ClassLoader loader 类加载器
-
Class<?>[] interfaces 把代理对象绑定在那些接口下
-
InvocationHandler 类中有一个invoke方法可以实现代理对象的逻辑
/** * Processes a method invocation on a proxy instance and returns * the result. This method will be invoked on an invocation handler * when a method is invoked on a proxy instance that it is * associated with. *proxy 代理对象 *method 被代理的方法 *args 被代理方法的参数 */ public Object invoke(Object proxy, Method method, Object[] args)
那么ProxyBean使用一个方法获取到proxy的实例
public static Object getProxyBean(Object target, Interceptor interceptor)
该方法就有两个约定
- target对象必须存在接口
- 返回一个proxy对象,可以根据target实现的接口对他进行强制转换
ProxyBean就需要有如下两个条件
- 需要有参数target(必须实现接口)和Interceptor(定义了流程)
- 同时要继承InvocationHandler实现invoke方法,这样当我们获取到代理对象执行方法是就可以在Invoke中实现约定的流程
ProxyBean的具体实现
public class MyProxyBean implements InvocationHandler {
private Object target;//保存被代理的对象
private Interceptor interceptor;//保存需要织入的流程
/**
* 绑定代理对象
* @param target 被代理的对象
* @param interceptor 拦截器
* @return 代理对象
*/
public static Object getProxyBean(Object target, Interceptor interceptor){
MyProxyBean myProxyBean = new MyProxyBean();
myProxyBean.target = target;
myProxyBean.interceptor = interceptor;
return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),myProxyBean);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
boolean hasException = false;//异常标志位
Object returnObj = null;
Invocation invocation = new Invocation(args,method,target);//可以通过它来实现被代理对象的方法的调用
try {
if (this.interceptor.before()){//需要
returnObj = this.interceptor.around(invocation);//执行around取代原有的方法
}else {
returnObj = method.invoke(target,method);//直接调用方法
}
}catch (Exception e){
hasException = true;
}
this.interceptor.after();
if (hasException){
this.interceptor.afterThrow();//方法执行后存在异常时调用
}else {
this.interceptor.afterReturning();//方法执行后存在异常时调用
return returnObj;
}
return null;
}
}
测试代码(测试代码中就是按照之前getProxyBean的约定编程的,只要按照约定编程就能将代码织入事先约定的流程)
@Test
void proxyTest(){
HelloService helloService = new HelloServiceImpl();
MyInterceptor myInterceptor = new MyInterceptor();
HelloService proxyHelloService = (HelloService)MyProxyBean.getProxyBean(helloService, myInterceptor);
proxyHelloService.sayHello("yogurt");
System.out.println("====================name is null================================");
proxyHelloService.sayHello(null);
}
测试结果
1.3 总结
测试的结果告诉我们,提供一定的约定规则后,只要按照约定编程就能实现某些功能。spring Aop也是如此,它能通过与我们的约定,把对应的方法通过动态代理技术织入约定的流程。所以掌握spring Aop的根本就是掌握其对我们的约定规则。
2.AOP概念
2.1 为什么使用AOP
使用AOP主要是将一些与业务逻辑无关的代码复用,开发者可以集中关注于业务逻辑的开发
比如说,操作日志、安全检测、事务处理等等。
2.2 AOP术语以及流程
AOP术语
-
连接点 joint point
指被拦截的对象,但是spring只支持方法,这里指的是特定的方法,比如HelloService的sayHello()就是一个连接点,Aop通过动态代理将它织入对应的流程
-
切点 point cut
由于我们的切面不单单指一个方法,可能需要织入流程的方法有很多,所以可以切点可以通过正则表达式去匹配这些方法
-
通知 advice
就是按照约定的流程下的方法,分为前置通知( before advice)、后置通知( afteradvice)、环绕通知( around advice)、事后返回通知(after Returning advice)和异常通知(after Throwing advice),它会根据约定织入流程中,需要弄明白它们在流程中的顺序和运行 的条件。
-
目标对象 target
被代理的对象,例如HelloServiceImpl
-
引入 introduction
指引入新的类和其方法,增强现有Bean的功能。
-
织入 weaving
它是一个过程,过程就是通过动态代理技术为原有的服务生成代理对象,将与切点匹配的连接点拦截,并按照约定将各类通知织入流程
-
切面 aspect
它是一个可以定义切点,通知和引入的内容,spring AOP可以通过它来增强bean的功能或者将对应的方法织入流程
下图解释了spring aop的流程约定
3.AOP的使用
3.1 切面、切点、通知
需求:UserServiceImpl的printUser方法织入流程
UserService和UserServiceImpl
public interface UserService {
void printUser(User user);
}
@Service
public class UserServiceImpl implements UserService{
@Override
public void printUser(User user) {
System.out.println(user.toString());
}
}
定义切面、切点、通知
@Aspect
@Component
public class MyAspect {
@Pointcut("execution(* com.yogurt.chapter4.aop.service.UserServiceImpl.printUser(..))")
public void pointCut(){
}
@Before("pointCut()")
public void before(){
System.out.println("before ....");
}
@After("pointCut()")
public void after(){
System.out.println("after ....");
}
@Around("pointCut()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("around before ....");
Object result = pjp.proceed();
System.out.println("around after ....");
return result;
}
@AfterReturning("pointCut()")
public void afterReturn(){
System.out.println("afterReturn ....");
}
@AfterThrowing("pointCut()")
public void afterThrowing(){
System.out.println("afterThrowing ....");
}
}
使用@PointCut来定义切点他的配置值是一个正则式
execution(* com.yogurt.chapter4.aop.service.UserServiceImpl.printUser(..))
- execution表示在执行的时候拦截正则匹配的方法
- *表示返回任意类型
- com.yogurt.chapter4.aop.service.UserServiceImpl类的全限定名
- printUser(..)表示任意参数的printUser方法
对于这个正则式而言还可以使用AspectJ指示器
上面的正则式也可以这样表达
@Pointcut("execution(* com.yogurt.chapter4.*.*.*.printUser(..)) && bean(userServiceImpl)")
bean中的内容表示对Spring Bean的限定
3.2 环绕通知
@Around("pointCut()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("around before ....");
Object result = pjp.proceed();
System.out.println("around after ....");
return result;
}
环绕通知非常强大,他的参数ProceedingJoinPoint pjp是一个被spring封装过的对象,他的proceed方法可以回调原有目标对象的方法,断点调试可以知道这个对象持有tagart、method、param,这样可以通过反射来调用目标对象的方法了。
3.3 引入
前面的例子只要传入的user为null会抛出异常,执行afterThrowing方法,那么我现在的需求是引入一个接口增强UserService使传过来的user为空时不执行printUser方法。
定义一个UserValidator接口和对应的实现类
public interface UserService {
void printUser(User user);
}
public class UserValidatorImpl implements UserValidator{
@Override
public boolean validate(User user) {
return user != null;
}
}
将接口加入切面
public class MyAspect {
@DeclareParents(value = "com.yogurt.chapter4.aop.service.UserServiceImpl+"
,defaultImpl = UserValidatorImpl.class)
private UserValidator userValidator;
这里使用 @DeclareParents引入新的接口来增强服务有两个配置项
- value 指向你要增强的目标对象,全限定名加+号
- defaultImpl 指向引入接口的默认实现类
测试
@Test
void test2() {
User user = new User();
user.setUserName("yogurt");
user.setNote("always");
//强制转换
UserValidator userValidator = (UserValidator) userService;
//判断user是否为空
if (userValidator.validate(user)) {
userService.printUser(user);
}
}
userService能强制转换为UserValidator实例的原理:
jdk的proxy类的newProxyInstance方法可以传入多个接口
public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)
让该代理对象挂在这两个接口下,且这些接口代理类能自由切换并使用他们的方法