0%

Spring春之虐我千百遍

案例分析

当需要为一个程序添加日志记录时,比如要记录下面方法的输入输出。

1
public class Calculator {//计算器对象
2
    public int add(int i, int j) {//加法
3
        System.out.println(i+","+j);
4
        int result=i+j;
5
        System.out.println("结果:"+result);
6
        return result;
7
    }
8
}
9
@Test
10
public void testC(){
11
	Calculator cal=new Calculator();
12
	cal.add(2,2);
13
}

假装以sout()来记录,这样的代码将日志记录与业务逻辑写在一起,耦合度太高,而且,当方法很多时,要一个一个加记录,很不方便,所以使用动态代理的方式来解决这一问题。

动态代理(JDK)

newProxyInstance()创建动态代理对象

新建一个代理对象CalculatorProxy

1
public class CalculatorProxy {
2
    public static Calculator getProxy(final Calculator calculator){
3
        ClassLoader classLoader=calculator.getClass().getClassLoader();
4
        //被代理对象的类加载器
5
        Class<?>[] interfaces=calculator.getClass().getInterfaces();
6
        //被代理对象实现的接口
7
        InvocationHandler h=new InvocationHandler() {
8
            //方法执行器
9
            @Override
10
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
11
                //proxy:代理对象,千万别动
12
                //method:目标对象的方法
13
                //args:方法调用时传的参数
14
                //利用反射执行目标方法
15
                Object result = method.invoke(calculator, args);
16
                //目标方法执行完的返回值
17
                return result;
18
            }
19
        };
20
        Object proxyInstance = Proxy.newProxyInstance(classLoader, interfaces, h);
21
        return (Calculator) proxyInstance;
22
    }
23
}

接着可以修改test方法

1
public void testC(){
2
    Calculator cal=new Calculator();
3
    Calculator proxy= CalculatorProxy.getProxy(cal);
4
    proxy.add(3,3);//代理对象执行方法
5
    proxy.sub(2,1);
6
}

然后要做日志记录就可以在代理对象里面做,比如Object result = method.invoke(calculator, args);的重要位置加上详细记录:

方法执行前,方法正常执行完,方法抛出异常,方法最终结束。

1
Object result = null;
2
try {
3
    System.out.println(method.getName()+"方法开始执行,参数列表为:"+ Arrays.toString(args));
4
    result = method.invoke(calculator, args);
5
    System.out.println(method.getName()+"方法正常执行完毕,结果为:"+ result);
6
} catch (Exception e) {
7
    System.out.println(method.getName()+"方法执行抛出异常"+e);
8
} finally {
9
    System.out.println(method.getName()+"方法最终执行结束");
10
}

结果:

在做了动态代理之后,很好的完成了解耦。

最后:可以用一个工具类来做日志记录,配置4个方法对应四个需要做日志记录的地方,将sout()替换成四个方法。

1
//单独抽出一个工具类专门做日志记录
2
public class LogUtil {
3
    public static void start(Method method,Object... args){
4
        System.out.println(method.getName()+"方法开始执行,参数列表为:"+ Arrays.toString(args));
5
    }
6
    public static void stop(Method method,Object result){
7
        System.out.println(method.getName()+"方法正常执行完毕,结果为:"+ result);
8
    }
9
    public static void exception(Method method,Exception e){
10
        System.out.println(method.getName()+"方法执行抛出异常"+e.getCause());
11
    }
12
    public static void end(Method method){
13
        System.out.println(method.getName()+"方法最终执行结束");
14
    }
15
}
16
//修改代理对象执行方法的日志记录
17
try {
18
	LogUtil.start(method,args);
19
	result = method.invoke(calculator, args);
20
	LogUtil.stop(method,result);
21
} catch (Exception e) {
22
	LogUtil.exception(method,e);
23
} finally {
24
	LogUtil.end(method);
25
}

产生的问题

写起来麻烦

是真的麻烦。

对象必须实现接口

如果对象没有实现任何接口,则无法创建代理对象。

代理对象和被代理对象唯一的关联就是视线里同样的接口。

所以Spring实现了AOP

一行代码都不写创建动态代理,不要求代理对象实现接口。

AOP(面向切面编程)

程序运行期间,将某段代码动态切入到指定方法的指定位置并运行。

一些概念:

以计算器日志为例。

计算器的计算方法四个日志记录点,称之为横切关注点

在记录点做记录的方法,称之为通知方法

通知方法属于切面类

每一个方法所有的记录点统称为连接点

有的点位不需要做记录,比如加减法的异常点,不会抛出异常;有的点位需要做记录,比如除法里除数为0会抛出异常,所以在方法执行抛出异常点,这样需要做记录的点位称之为切入点

通过什么方法判定一个点位是不是切入点,这样的方法称之为切入点表达式

导坐标

1
<dependency>
2
	<groupId>org.springframework</groupId>
3
	<artifactId>spring-aspects</artifactId>
4
	<version>5.2.3.RELEASE</version>
5
</dependency>
6
<dependency>
7
    <groupId>cglib</groupId>
8
    <artifactId>cglib</artifactId>
9
    <version>3.3.0</version>
10
</dependency>
11
<dependency>
12
    <groupId>org.aspectj</groupId>
13
    <artifactId>aspectjweaver</artifactId>
14
    <version>1.9.5</version>
15
</dependency>
16
<dependency>
17
    <groupId>aopalliance</groupId>
18
    <artifactId>aopalliance</artifactId>
19
    <version>1.0</version>
20
</dependency>

存入IoC容器

给目标类和切面类加上@Component注解

给切面类加上@Aspect注解

托付给Spring

告知Spring,每个方法在何时何地运行

几个注解

@Before:前置通知,方法运行前

@AfterReturning:返回通知,方法正常返回之后

@AfterThrowing:异常通知,方法抛出异常之后

@After:后置通知,方法最终结束

@Around:环绕通知,

以@Before为例:

@Before(“execution(访问修饰符 返回值类型 方法签名)”)

1
@Before("execution(public int com.czw.bean.impl.MyCalculator.add(int,int))")
2
//将add改成*表示该类下同修饰符,同返回值,同参数列表的所有方法

开启基于注解的AOP

导入aop名称空间

1
xmlns:aop="http://www.springframework.org/schema/aop"
2
<!--将下面两句加入xsi:schemaLocation-->
3
http://www.springframework.org/schema/aop
4
http://www.springframework.org/schema/aop/spring-aop.xsd

测试

1
@Autowired
2
private ICalculator myCalculator;
3
//注入bean
4
@Test
5
public void testC(){
6
	myCalculator.add(2,2);
7
}

结果完美

细节

aop底层就是动态代理,保存在容器中的是代理对象

1<对象类型

表面看起来是注入的bean对象,实际类型是代理对象类型。

2<不用实现接口

MyCalculator没有实现Calculator接口,也可以做到动态代理,容器中的类型就是本类型,cglib会创建实现类所有方法的内部类,最后还是一个代理对象。

3<切入点表达式写法

execution(访问修饰符 返回值类型 方法全类名(参数列表))

execution(public int com.czw.bean.impl.MyCalculator.add(int,int))

这是最精确的表示,还可以使用通配符模糊匹配

“ * ” :匹配一个或多个字符/任意一个参数/任何类型返回值

execution(public * com.*.bean.impl.MyCal*.*(int,*))

任意修饰符不能用*表示,不写就是任意修饰符(好像只要public可以选)

如果只有一个*,那就是代表整个工程下所有类所有方法

execution(* *(..)) :这个就是全方位封锁打击

“ .. “ :匹配任意多个任意类型参数/任意多层路径

execution(public int com..MyCal*.*(..))

**” && 、||、! “ **:同时满足,满足任意,不满足

4<通知方法执行顺序

正常执行时:@Before–>@After–>@AfterReturning

异常执行时: @Before–>@After–>@AfterThrowing

5<获取方法详细信息

在通知方法参数上写一个JoinPoint,表示方法详细信息

1
@Before("execution(public int com.czw.bean.impl.MyCalculator.*(int,int))")
2
public static void start(JoinPoint joinPoint){
3
    Object[] args = joinPoint.getArgs();
4
    //获取输入参数
5
    Signature signature = joinPoint.getSignature();
6
    //signature表示方法前面,和execution里内容一样,包括修饰符、返回值、全类名、方法名、参数列表。
7
    System.out.println(signature.getName()+"方法开始执行,参数列表为:"+ Arrays.toString(args));
8
    //System.out.println("方法开始执行前");
9
}
10
//获取方法返回值,用通知注解的returning属性可以指定参数列表里的参数为返回值
11
@AfterReturning(value="execution(xxx)",returning="result")
12
public static void stop(JoinPoint joinPoint,Object result){
13
	Signature signature = joinPoint.getSignature();
14
	System.out.println(signature.getName()+"方法正常执行完毕,结果为:"+ result);
15
	//System.out.println("方法正常执行完成");
16
}
17
//异常同理用throwing指定

6<Spring对通知方法不严格

唯一要求就是方法的参数列表不能乱写,通知方法是Spring利用反射调用,所以参数列表必须明确,要让Spring知道是什么。接受异常和返回值范围要写大。

7<抽取可重用切入点表达式

先声明一个没有实现的void空方法,加上@Pointcut注解,加上表达式,其他用到表达式的地方用方法名代替即可。

1
@Pointcut("execution(xxx)")
2
public void point();
3
4
@Before("point()")
5
public ...{
6
    ...
7
}

8<环绕通知

Spring中最强大的通知,基本就是动态代理,等于其他四合一。

1
@Around("point()")
2
public Object myAround(ProceedingJoinPoint pjp) throws Throwable {
3
    //pjp是JoinPoint的子接口,继承了所有方法
4
    Object[] args = pjp.getArgs();
5
    String name = pjp.getSignature().getName();
6
    Object proceed = null;
7
    try {
8
        System.out.println("Around-Before:"+name+"方法开始执行,参数为"+Arrays.toString(args));
9
        proceed = pjp.proceed(args);
10
        //这一句直接就是method.invoke(obj,args);要加通知在这句周围加就行
11
        System.out.println("Around:-AfterReturning"+name+"方法正常执行完毕,返回值:"+proceed);
12
    } catch (Exception e) {
13
        System.out.println("Around:-AfterThrowing"+name+"方法抛出异常,异常原因:"+e.getCause());
14
    } finally {
15
        System.out.println("Around-After:"+name+"方法最终执行结束");
16
    }
17
    return proceed;//这个直接就是方法返回值
18
}

十分顺利,并且顺序是正确的。

9<环绕通知与其他共存时

环绕通知执行顺序优先其他通知

环绕前置&普通前置–>目标方法–>环绕返回/异常–>环绕后置–>普通返回/异常–>普通后置

注意:出现异常时,环绕通知要把异常抛出去,不然普通通知就捕捉不到异常了

10<多切面执行顺序

没有环绕通知时,类似于层层包装,从一端到令一端,中间是目标方法,先执行切面A的前置,然后切面B前置,然后目标方法,然后切面B正常/异常,后置,然后切面A…具体哪个在外边由切面类首字母排序A最外面,Z最里面。

@Order注解加上一个int类型值,可以指定优先级,越小越优先,在最外层

当外切面加了环绕通知时,也只是在当前切面位置 贴着内切面,而不是贴着目标方法。

AOP使用场景

日志记录,权限验证,事务控ZHI

基于XML的AOP

加入容器

用bean标签配上id和class(切面类+目标类)

1
<bean id="LogUtil" class="xxx"></bean>
2
<bean id="MyCalculator" class="xxx"></bean>

相当于加上@Component注解

告知Spring切面类

使用aop名称空间指定

1
<aop:config>
2
	<aop:aspect ref="LogUtil"></aop:aspect>
3
    ...
4
</aop:config>

相当于加上@Aspect注解

配置运行时间和位置

加入到< aop:config >标签的< aop:aspect >中,aspect标签也有order属性

1
<aop:pointcut id="mypoint" expression="execution(xxx)"/>
2
<!--声明在aspect标签里直供本切面使用,要想所有切面使用,可以声明在config标签里-->
3
<aop:before method="start" pointcut="execution(xxx)"/>
4
<!--直接写表达式-->
5
<aop:afterreturning method="stop" pointcut-ref="mypoint" returning="result"/>
6
<!--引用抽取出来的表达式,并指定返回值/异常对应方法的参数-->
7
<aop:afterthrowing method="exception" pointcut-ref="mypoint" throwing="exception"/>
8
<aop:after method="end" pointcut="execution(xxx)"/>
9
<aop:around method="myAround" pointcut-ref="mypoint" />

注意:在XML配置文件中,配置的顺序决定了通知的顺序,先配的切面在外层,后配的切面在里层。并且,切面中环绕如果先配,环绕前置在前面,环绕如果后配,普通前置在前面。

对比

注解:快速方便

XML:功能完善

重要的用配置,不重要的用XML

总结

头发-1