Spring4.3.8学习[三]

2017-06-19  本文已影响93人  小椰子表姐

如果转载文章请注明出处, 谢谢 !
本系列文章是学习完 Spring4.3.8 后的详细整理, 如果有错误请向我指明, 我会及时更正~😝

Spring4.3.8

Spring4.3.8学习[一]
Spring4.3.8学习[二]

4. 面向切面编程

这里有一张网上看到的图我觉得也蛮适合理解的, 可以看完下面的代理之后返回来仔细看看这张图能够更容易理解:


AOP

4.1 代理模式

我们在 Hibernate 中处理增删改功能都要添加事务, 代码如下:

public class UserDaoImpl implements UserDao {
    @Override
    public void insertUser() {
        Session session = sessionFactory.openSession();
        Transaction transaction = session.beginTransaction();

        System.out.println("插入用户");

        transaction.commit();
        session.close();
    }

    @Override
    public void delete(Integer uid) {
        Session session = sessionFactory.openSession();
        Transaction transaction = session.beginTransaction();

        System.out.println("删除用户");

        transaction.commit();
        session.close();
    }
}

事务和逻辑的处理混杂在一起, 如果再增添方法还需要继续重复有关事务的重复代码.
或者需求发生了变化,要求项目中所有的类在执行方法时输出执行耗时。最直接的办法是修改源代码
缺点:
1、工作量特别大,如果项目中有多个类,多个方法,则要修改多次。
2、违背了一些设计原则:
开闭原则(OCP)[对扩展开放,对修改关闭,而为了增加功能把每个方法都修改了,也不便于维护。]
单一职责(SRP)[每个方法除了要完成自己本身的功能,还要计算耗时、延时;每一个方法引起它变化的原因就有多种。]

4.1.1 静态代理

1、定义UserDao接口, 抽象主题

public interface UserDao{
    void insertUser();
    void delete(Integer uid);
}

2、实现UserDao接口, 被代理的目标对象, 真实主题

public class UserDaoImpl implements UserDao {
    @Override
    public void insertUser() {
        System.out.println("插入用户");

    }
    @Override
    public void delete(Integer uid) {
        System.out.println("删除用户");

    }
}

3、代理类, 静态代理类

public class UserDaoProxy implements UserDao{
      //被代理的对象
    private UserDao userDao;
    private Transaction transaction;

    public UserDaoProxy(UserDao userDao, Transaction transaction) {
        this.userDao = userDao;
        this.transaction = transaction;
    }

    @Override
    public void insertUser() {
        transaction.beginTransaction();
        userDao.insertUser();
        transaction.commitTransaction();
    }

    @Override
    public void delete(Integer uid) {
        transaction.beginTransaction();
        userDao.insertUser();
        transaction.commitTransaction();
    }
}

4、测试

public class ProxyTest {
    @Test
    public void testProxy(){
        UserDao dao = new UserDaoImpl();
        Transaction transaction = new Transaction();
        UserDaoProxy proxy = new UserDaoProxy(dao, transaction);
        proxy.insertUser();
    }
}

通过静态代理可以解决这些设计原则问题, 但是不能解决:
如果项目中有多个类,则需要编写多个代理类,工作量大,不好修改,不好维护,不能应对变化.
如果要解决上面的问题,可以使用动态代理

4.1.2 JDK动态代理

JDK的动态代理主要涉及 java.lang.reflect 包中的两个类:Proxy和InvocationHandler。其中,InvocationHandler是一个接口,可以通过实现该接口定义横切逻辑,并通过反射机制调用目标类的代码,动态地将横切逻辑和业务逻辑编织在一起。而Proxy利用InvocationHandler动态创建一个符合某一接口的实例,生成目标类的代理对象。

只需要一个代理类,而不是针对每个类编写代理类。
1、在上一个示例中修改代理类 UserDaoProxy 如下:
JDK动态代理需要实现 InvocationHandler 接口, 这个类其实应该是拦截器类

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * 拦截器:
 *      1. 目标类导入
 *      2. 事务导入
 *      3. invoke完成:
 *          1.开启事务
 *          2.调用目标对象方法
 *          3.事务提交
 */
public class UserDaoProxy implements InvocationHandler {
    private Object target;
    private Transaction transaction;


    public UserDaoProxy(Object target, Transaction transaction) {
        this.target = target;
        this.transaction = transaction;
    }

    /**
     * 当用户调用对象中的每个方法时都通过下面的方法执行,方法必须在接口
     * proxy 被代理后的对象
     * method 将要被执行的方法信息(反射)
     * args 执行方法时需要的参数
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String methodName = method.getName();
        if (!methodName.equals("getUser")){
            this.transaction.beginTransaction();
            method.invoke(target, args); // 目标对象调用目标方法
            this.transaction.commitTransaction();
        } else {
            method.invoke(target, args);
        }
        return null;
    }

    /**
     * 获得被代理后的对象
     * @param object  被代理的对象
     * @return  代理后的对象
     * loader:一个ClassLoader对象,定义了由哪个ClassLoader对象来生成代理对象进行加载
     * interfaces:一个Interface对象的数组,表示的是我将要给我需要代理的对象提供一组什么接口,如果我提供了一组接口给它,那么这个代理对象就宣称实现了该接口(多态),这样我就能调用这组接口中的方法了
     * h:一个InvocationHandler对象,表示的是当我这个动态代理对象在调用方法的时候,会关联到哪一个InvocationHandler对象上,间接通过invoke来执行
     */
    public Object getProxyObject(Object object){
        this.target = object;
        return Proxy.newProxyInstance(
                target.getClass().getClassLoader(), //类加载器
                target.getClass().getInterfaces(),//获得被代理对象的所有接口
                this);  //拦截器: InvocationHandler对象
    }
}

2、测试一下:

public class JDKProxyTest {
    @Test
    public void testProxy(){
        /*
            1. 创建一个目标类
            2. 创建一个事务
            3. 创建一个拦截器
            4. 动态产生代理对象
         */
        Object target = new UserDaoImpl();
        Transaction transaction = new Transaction();
        List<Interceptor> interceptorList = new ArrayList<>();
        interceptorList.add(transaction);
        UserDaoProxy interceptor = new UserDaoProxy(target, transaction);

      UserDao dao = (UserDao) interceptor.getProxyObject(target);
        dao.delete("10");
    }
}

使用内置的Proxy实现动态代理有一个问题:被代理的类必须实现接口,未实现接口则没办法完成动态代理。
如果项目中有些类没有实现接口,则不应该为了实现动态代理而刻意去抽出一些没有实例意义的接口,通过cglib可以解决该问题。

4.1.3 CGLIB动态代理

CGLIB(Code Generation Library)是一个开源项目,是一个强大的,高性能,高质量的Code生成类库,它可以在运行期扩展Java类与实现Java接口,通俗说cglib可以在运行时动态生成字节码。
1、引用 cglib jar 包,通过maven或者直接下载 jar 包添加

CGLIB jars

下载地址: http://download.csdn.net/download/javawebxy/6849703
maven:<dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>2.2.2</version> </dependency>

2、修改 拦截器类

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;

/*
 * 动态代理类
 * 实现了一个方法拦截器接口
 */
public class MyInterceptor implements MethodInterceptor {
    // 被代理对象
    private Object target;
    private Transaction transaction;

    public MyInterceptor(Transaction transaction) {
        this.transaction = transaction;
    }

    //动态生成一个新的类,使用父类的无参构造方法创建一个指定了特定回调的代理实例
    public Object getProxyObject(Object object) {
        this.target = object;
        // 代码增强类
        Enhancer enhancer = new Enhancer();
        // 回调方法
        enhancer.setCallback(this); // 参数就是拦截器
        //设置生成类的父类类型
        enhancer.setSuperclass(target.getClass());// 生成的代理类父类是目标类
        //动态生成字节码并返回代理对象
        return enhancer.create();
    }

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        this.transaction.beginTransaction();
        method.invoke(target, objects);
        this.transaction.commitTransaction();
        return null;
    }
}

3、测试

/**
 * 通过 CGLIB 产生的代理类是目标类的子类
 */
public class CGLIBProxyTest {
    @Test
    public void testCGLIB(){
        Object target = new UserDaoImpl();
        Transaction transaction = new Transaction();
        MyInterceptor interceptor = new MyInterceptor(transaction);

       UserDaoImpl dao = (UserDaoImpl) interceptor.getProxyObject(target);
       dao.insertUser();

        //另一个被代理的对象,不再需要重新编辑代理代码
        PersonDaoImpl personDao = (PersonDaoImpl) interceptor.getProxyObject(new PersonDaoImpl());
        personDao.delete();
    }
}

4.2 AOP编程

4.2.1概念

使用 JDK 动态代理的代码进行对应说明
1. Aspect(切面)
比如说事务、权限等,与业务逻辑没有关系的部分

切面

2. joinPoint(连接点)
目标类的目标方法。(由客户端在调用的时候决定)

连接点

3. pointCut(切入点)
所谓切入点是指我们要对那些拦截的方法的定义.
被纳入spring aop中的目标类的方法。

4. Advice(通知)
所谓通知是指拦截到joinpoint之后所要做的事情就是通知.通知分为前置通知,后置通知,异常通知,最终通知,环绕通知(切面要完成的功能)

通知

5. Target(目标对象)
代理的目标对象
6. Weaving(织入)
是指把切面应用到目标对象来创建新的代理对象的过程.切面在指定的连接点织入到目标对象

对比表格查看

JDKProxy代理 SpringAop
目标对象 目标对象 Target
拦截器类 切面 Aspect
拦截器类中的方法 通知 Advice
被拦截到的目标类中方法的集合 切入点 pointCut
在客户端调用的方法(目标类目标方法) 连接点 joinPoint
代理类 AOP代理 AOP Proxy
代理类的代理方法生成的过程 织入 Weaving

4.2.2 Spring AOP实现的两种模式

4.2.2.1 xml形式

还以事务为例, 准备 Transaction 切面类, 目标接口类 UserDao, 目标类 UserDaoImpl

/**
 * 切面
 */
public class Transaction {
    public void beginTransaction(){
        System.out.println("Begin Transaction");
    }

    public void commit(){
        System.out.println("Commit Transaction");
    }
}

- - - - - - - -


public interface UserDao {
    void saveUser();
}

- - - - - - - -

public class UserDaoImpl implements UserDao {
    @Override
    public void saveUser() {
        System.out.println("save Person");
    }
}

剩余的我们需要配置 appliactionContext.xml

1、引入 aop 命名空间

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd" >

2、配置 两个 bean

<bean id="userDao" class="com.lanou.spring.aop.transaction.UserDaoImpl"/>
<bean id="transaction" class="com.lanou.spring.aop.transaction.Transaction"/>

3、aop 的配置

<!-- aop 配置-->
<aop:config>
    <!-- 切入点表达式, 确认目标类 -->
    <aop:pointcut id="pointcut" expression="execution(* com.lanou.spring.aop.transaction.UserDaoImpl.*(..))"/>
    <!-- ref 指向的对象就是切面 -->
    <aop:aspect ref="transaction">
        <!-- 前置通知-->
        <aop:before method="beginTransaction" pointcut-ref="pointcut" />
        <!-- 正常返回通知-->
        <aop:after-returning method="commit" pointcut-ref="pointcut"/>
    </aop:aspect>
</aop:config>

4、测试

public class TransactionTest {
    @Test
    public void testTransaction(){
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        UserDao userDao = (UserDao) context.getBean("userDao");
        userDao.saveUser();
    }
}

5、报错, Spring缺少 aspectjweaver.jar 包, 添加 jar 包重新运行

Caused by: java.lang.ClassNotFoundException: org.aspectj.weaver.reflect.ReflectionWorld$ReflectionWorldException
    at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    ... 70 more

Spring AOP 原理:

  1. 当spring容器启动的时候,加载两个bean,对两个bean进行实例化
  2. 当spring容器对配置文件解析到<aop:config>的时候
  3. 把切入点表达式解析出来,按照切入点表达式匹配spring容器内容的bean
  4. 如果匹配成功,则为该bean创建代理对象
  5. 当客户端利用context.getBean获取一个对象时,如果该对象有代理对象,则返回代理对象; 如果没有代理对象,则返回对象本身
  6. 切入点表达式如与springbean 没有一个匹配就会报错

4.2.2.2 注解形式

  1. 首先需要导入两个 jar 包 : aspectjrt & aspectjweaver . 需要注意如果你的 jdk 是1.7 就找这两个 jar 包的1.7.+版本; 我是jdk1.8, 使用了aspectjrt1.8.5 & aspectjweaver1.8.5
  2. 在 applicationContext.xml 中添加context, aop 的命名空间.扫描, 添加自动创建代理设置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd" >

    <!-- 扫描包 -->
    <context:component-scan base-package="com.lanou.transaction"/>
    <!-- 自动创建代理 -->
    <aop:aspectj-autoproxy/>
</beans>
  1. 添加 UserDaoImpl, Transaction 的注解
@Repository("userDao")
public class UserDaoImpl implements UserDao {...}
--------
@Component
public class Transaction {...}
  1. 在 Transaction 切面类中添加注解
@Component
@Aspect
public class Transaction {
    @Pointcut("execution(* com.lanou.transaction.UserDaoImpl.*(..))")
    private void method(){} //方法签名
    
    @Before("method()")
    public void beginTransaction(JoinPoint joinpoint){
        System.out.println("Begin Transaction");
    }
    
    @AfterReturning(value = "method()", returning = "returnVal")
    public void commit(JoinPoint joinpoint, Object returnVal)
    {
        System.out.println("返回值: " + returnVal);
        System.out.println("Commit Transaction");
    }

    @After("method()")
    public void finallyMethod(JoinPoint joinpoint){
        System.out.println("Finally Method");
    }

    @AfterThrowing(value = "method()", throwing = "throwable")
    public void throwingMethod(JoinPoint joinpoint, Throwable throwable){
        System.out.println("异常: " + throwable.getMessage());
    }
}

4.2.3 切入点表达式

屏幕快照 2017-06-08 下午4.38.24.png
execution(public * *(..)) :  任意公共方法的执行
execution(* set*(..))  : 任何一个名字以 set 开始的方法的执行
execution(* com.lanou.spring.aop.transaction.service.*.*(..))  :  在 service 包中定义的任意方法的执行
execution(* com.lanou.spring.transaction.service..*.*(..))  :  在 service 包或其子包中定义的任意方法的执行
execution(* com.lanou.spring.aop..service..*.*(..))  : 在 aop 包及子包一直到 service 包,再子包下的所有类所有方法 

4.2.4 通知

4.2.4.1 通知种类

名称 解释 使用
前置通知
[Before advice]
在连接点前面执行,前置通知不会影响连接点的执行,
除非此处抛出异常
<aop:before method="before" pointcut-ref="pointcut"/>
正常返回通知
[After returning advice]
在连接点正常执行完成后执行,
如果连接点抛出异常,则不会执行
<aop:after-returning method="afterReturning" pointcut-ref="pointcut"/>
异常返回通知
[After throwing advice]
在连接点抛出异常后执行 <aop:after-throwing method="afterThrowing" pointcut-ref="pointcut"/>
返回通知
[After (finally) advice]
在连接点执行完成后执行,
不管是正常执行完成,还是抛出异常,都会执行返回通知中的内容
<aop:after method="after" pointcut-ref="pointcut"/>
环绕通知
[Around advice]
环绕通知围绕在连接点前后,比如一个方法调用的前后。
这是最强大的通知类型,能在方法调用前后自定义一些操作。
环绕通知还需要负责决定目标方法的执行
<aop:around method="around" pointcut-ref="pointcut"/>

4.2.4.2 通知使用细节

  1. 前置通知: 每种通知都能够添加连接点参数, 可以获取连接点信息
/*
    前置通知
        1.在目标方法执行之前执行
        2.获取不到目标方法返回值
 */
public void beginTransaction(JoinPoint joinpoint){
    System.out.println("连接点名称: "+joinpoint.getSignature().getName());
    System.out.println("目标类" + joinpoint.getTarget().getClass());

    System.out.println("Begin Transaction");
}
  1. 后置通知可以获取返回值类型, 但当目标方法产生异常, 后置通知将不再执行
public class UserDaoImpl implements UserDao {
       @Override
       public String saveUser() {
           /*  制造异常
               int a = 1/0;
           */
           System.out.println("save User");
           return "11111111";
       }
}
 <!-- 正常返回通知
        1. 可以获取目标方法的返回值
        2. 当目标方法产生异常, 后置通知将不再执行
   -->
   <aop:after-returning method="commit" pointcut-ref="pointcut" returning="returnVal"/>
</aop:aspect>
/*
   后置通知, 在目标方法执行之后执行, 返回值参数的名称与 xml 中保护一致
*/
public void commit(JoinPoint joinpoint, Object returnVal){
       System.out.println("目标方法返回值: " + returnVal);
       System.out.println("Commit Transaction");
}
  1. 最终通知
/*
   最终通知
   无论目标方法是否发出异常都将执行
*/
public void finallyMethod(JoinPoint joinpoint){
   System.out.println("Finally Method");
}
<!-- 最终通知 -->
<aop:after method="finallyMethod" pointcut-ref="pointcut"/>
  1. 异常通知
/*
    异常通知
    接受目标方法抛出的异常
 */
public void throwingMethod(JoinPoint joinPoint, Throwable throwable){
    System.out.println("异常: " + throwable.getMessage());
}
<!--异常通知-->
<aop:after-throwing method="throwingMethod" pointcut-ref="pointcut" throwing="throwable"/>
  1. 环绕通知
/*
    环绕通知
    ProceedingJoinPoint: 子接口
    控制目标方法的执行
    前置通知和后置通知也能在目标方法的前后添加内容,但是不能控制目标方法的执行
 */
public void around(ProceedingJoinPoint joinPoint) throws Throwable {
    System.out.println("before..before..before");
    joinPoint.proceed(); // 调用目标方法
    System.out.println("after..after.after");
}
<!-- 环绕通知 -->
    <aop:around method="around" pointcut-ref="pointcut"/>
</aop:aspect>

4.2.5 SpringAOP 细节

  1. 如果目标类实现了接口, 则采用 JDKProxy; 如果没有实现接口, 采用 CGLIBProxy [Spring 内部做的]
  2. 目标类实现了接口, 但还想要采用 CGLIBProxy, 作如下更改:

Spring4.3.8学习之 与 Struts2 整合[四]
Spring4.3.8学习之与Hibernate4 整合[五]
Spring4.3.8学习之S2SH 整合[六]

上一篇下一篇

猜你喜欢

热点阅读