什么是aop?

这个命题其实是讲了的,但是之前没有做,发现一些面试会问到,结合自己之前的学习经历。简单把这个问题描述一下。

aop是跟oop相对应的一个概念。分别是aspect oriented programming 和 object oriented programming。也即面向切面编程和面向对象编程。那面向对象是我们一个很熟悉的概念了,【在面向对象之前还有面向过程的编程,这个概念这里面就不厘了】。画个示意图,希望能够阐述这样一个概念。

那么对于前面的就是得到一个对象,或者得到一个完整的过程,直接对他进行操作。【事实上前面儿这个图画的也不理想】

对于后面的图来说,面向的是整个过程的一个剖儿面。

比如说下面的存储过程。

原来的逻辑是 一个数据库的存储操作,现在,我们要在这个 完成的逻辑上面添加一些日志,比如说,在存储之前,我们让日志信息记录说,UserDaoImpl请求添加一条记录到数据库中,在存储操作完成以后,让日志信息再打印一个 UserDaoImpl已经成功添加了一条数据。

对于这个完整过程来说,原来的逻辑是完完全全没有问题的。但是如果在一个大型系统中出了问题,如果在每个地方都有相应的提示信息的话,可以帮助我们快速定位到问题从而顺利解决。所以如果打印到“UserDaoImpl请求添加一条记录到数据库中”,然后报了错,下面那句话没有执行,是不是就很方便可以定位到存储时候出了问题。【这里只是举个例子说明织入也就是aop这个概念。之前dao都没出过问题,加上了切面反而出了问题,那还要这个切面干毛线?这个假设简直不能再糟糕,笑哭】【想到一个可能的逻辑,囧,比如在oracle里面的年龄字段写了check 18 to 60,而在前端的校验只定义成了0-150合理,这样在junit测试里面没有把例子跑全,或者自己只写了一个简单的测试用例比如数字都是23,34,59。导致在集成测试的时候有一个78被放行,这样就有可能出问题,在修改的时候可能回去数据库里面更改check 或者 去js验证里面,更改放行数字。终于圆回来了,反正总之现在的需求就是加一个aop!!!】

 

好了问题引入完毕,先来扯点儿蛋。

aop的知识点是从代理模式引入的。

那么什么是代理,为什么要代理。代理是怎么一回事儿?

代理分为两种形式,静态代理,动态代理。

假设有一个接口UserDao,【这个包名我就很喜欢】

package com.letben.dao;

/**
 * userdao的 接口
 * @author Administrator
 *
 */
public interface UserDao {
    public void saveUser();
}

还有一个它的实现类UserDaoImpl。

package com.letben.dao;
/**
 * 这个地方用来实现userdao的 真实逻辑
 * @author Administrator
 */
public class UserDaoImpl implements UserDao {
    @Override
    public void saveUser() {
        System.out.println("存储用户到数据库");
    }
}

那原来这两个就能完成存储逻辑。

现在需求改变我们要加一个代理,实现在存储之前之后打印日志。那么新写一个UserDaoStaticProxy

package com.letben.dao;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
 * 同样是userdao的一个 实现类,没有更多的业务逻辑。只是为了在 实现逻辑的前后 打印日志
 * @author Administrator
 */
public class UserDaoStaticProxy implements UserDao {
    //得到打印日志的对象,它属于util包
    Logger logger = Logger.getLogger(UserDaoStaticProxy.class.getName());
    UserDao userDao;
    public UserDaoStaticProxy(UserDao userDao){
        this.userDao = userDao;
    }
    @Override
    public void saveUser() {
        logger.log(Level.INFO, "存储用户之前");
        userDao.saveUser();
        logger.log(Level.INFO,"存储用户之后");
    }
}

然后添加一段测试代码:

package com.letben.dao;
public class Test {
    public static void main(String[] args) {
        UserDao userDao = new UserDaoImpl();
        UserDao userDaoApplication = new UserDaoStaticProxy(userDao);
        userDaoApplication.saveUser();
    }
}

运行结果:

二月 15, 2016 9:50:34 上午 com.letben.dao.UserDaoStaticProxy saveUser
信息: 存储用户之前
存储用户到数据库
二月 15, 2016 9:50:35 上午 com.letben.dao.UserDaoStaticProxy saveUser
信息: 存储用户之后

在测试代码中就能够理解是为什么了。我们首先创建了一个UserDaoImpl也就是最开始用来完成存储业务的类。然后又创建了UserDaoImpl来增加日志信息。这样aop的概念,就比较好解释了,就是在这个逻辑不改变的情况下,多那么一点点东西,让整个流程更加清楚、完善。

但是静态代理毕竟可复用性太差,要重新写java代码,这就比较糟糕。

那动态代理的部分,就是利用框架,来实现这样一种需求的嵌入。这还有两种形式,一种是xml的配置形式,还有一种是 注解的形式。

形式一:xml配置形式

创建web工程。

导入jar包。【没有的在下面写邮箱吧,或者小纸条我也行。】

目录结构。

1、那我们其实真正做的就是后来,导入了一些专用的jar包。

2、添加一个切面包。以及文件:AspectDemo.java。

package com.letben.aspect;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.aspectj.lang.ProceedingJoinPoint;
/**
 * 切面类
 * @author Administrator
 */
public class AspectDemo {
    Logger log = Logger.getLogger(AspectDemo.class.getName());
    /**
     *  在某断点之前添加通知,我们这里就是 给 添加方法 之前设置的 所以 这个 方法的名字,也是 这么取名的
     */
    public void beforeSaveUser(){
        log.log(Level.INFO, "存储前");
    }
    /**
     * 在saveUser方法 调用之后 执行此方法
     */
    public void afterSaveUser(){
        log.log(Level.INFO,"存储后");
    }
    public void aroundSaveUser(ProceedingJoinPoint point){
        log.log(Level.INFO,"环绕-前");
        try {
            point.proceed();
        } catch (Throwable e) {
            e.printStackTrace();
        }
        log.log(Level.INFO,"环绕-后");
    }
    public void exceptionAboutSave(Exception e){
        //在这里并不会有结果,因为 上一个 已经 处理了异常,并且 我们 的存储方法 是正确的 不存在 输入错误 这回事。。所以 为了巩固 我们再写一个 面向切面代理出错版本。
        log.log(Level.WARNING,"出现异常");
    }
}

3、在beans.xml里面进行配置。

<?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-2.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.0.xsd">


    <!-- 要有 对应 实现逻辑的 userDao -->
    <bean id="userDao" class="com.letben.dao.UserDaoImpl"></bean>

    <!-- 切面类 对应的 实体对象 -->
    <bean id="aspectDemo" class="com.letben.aspect.AspectDemo"></bean>
    <!-- aop的配置 -->
    <aop:config>
        
        <!-- 选定某一个类为切面 -->
        <aop:aspect ref="aspectDemo">
            <!-- 切入点表达式,就是 我们为了做切入,一定要有一个切入点,用来表示 在某一个地方开始 执行我们的某些程序-->
            <!-- 文档中 6.2.3.4里面的各个 示例 -->
            <!-- 解释:任意的修饰符  【空格】 这个 包下的 这个 方法里面的所有方法(这里面带着所有的参数)-->
            <aop:pointcut expression="execution(* com.letben.dao.UserDao.*(..))" id="point" />
            
            <!-- 通知类型+插入方法+切入点 =连接点 -->
            <aop:before method="beforeSaveUser" pointcut-ref="point" />
            <!-- 
                上面这句话有两种书写方式:
                <aop:before method="beforeSaveUser" pointcut="execution(* com.letben.dao.UserDao.*(..))">
                要么直接 写切入点,要么采用引入的方式 写 切入点
             -->
             <aop:after method="afterSaveUser" pointcut-ref="point"/>
             <aop:around method="aroundSaveUser" pointcut-ref="point"></aop:around>
            
        </aop:aspect>
    </aop:config>
</beans>

 

这样就完成了动态代理的xml形式的书写。

 

注解方式:

注解编程里面,配置就比较简单。

jar包还是那些。

beans.xml

<?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-2.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.0.xsd">

    <!-- 启动注解编程 -->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>

    <bean id="userDao" class="com.letben.dao.impl.UserDaoImpl"></bean>
    
    <!-- 因为 当前注解类 并不需要 给谁使用,所以 直接注册 并不需要 别的什么逻辑 -->
    <bean class="com.letben.aspect.AspectForUserDao"/>

</beans>

切面类:

package com.letben.aspect;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
/**
 * 声明这是一个 注解类
 * @author Administrator
 */
@Aspect
public class AspectForUserDao {
    Logger logger = Logger.getLogger(AspectForUserDao.class.getName());
    /**
     * 前置 方法 里面需要一个 切入点
     */
    @Before("execution(* com.letben.dao.UserDao.*(..))")
    public void test1(){
        logger.log(Level.INFO,"之前");
    }
    /**
     * 在7.0里面 那个 value 不能加上。但是 在 6.0里面这个 参数 是需要的
     * @Around(value="exection(*com.letben.dao.UserDao.*(..))")这是不正确的写法 在7.0里面
     */
    @After("execution(* com.letben.dao.UserDao.*(..))")
    public void test2(){
        logger.log(Level.INFO,"之后");
    }
    /**
     * 不,不是 那个 value 也不知道 是哪里,这个 注解编程 很奇怪,就是 很奇怪。我也不知道 那里 写错了,但是 就是 报了 一些 处理了20min的异常就在这个 写注解的地方
     * @param point
     */
    @Around(value="execution(* com.letben.dao.UserDao.*(..))")
    public void test3(ProceedingJoinPoint point){
        logger.log(Level.INFO,"环绕-前");
        try {
            point.proceed();
        } catch (Throwable e) {
            e.printStackTrace();
        }
        logger.log(Level.INFO,"环绕-后");
    }
}

 

所以动态的代理就是 我们所说的aop也就是织入了一个逻辑。织入这个词,感觉翻译起来还是很贴切的。就是在完整的东西上加进去一些东西,让他更加完美。上面是对日志的织入。

下面有事务的织入。略有不同。

只说修改的地方了,原来的那个工程还是有点儿小。。。

对于事务这样的操作,想来应该是放到service层里面最合适。比如存取钱的操作同时完成。不应该封装到dao层理面,而应该是 服务层理面,但是其实,这样的操作也可在dao里面写,但是框架提供了这样的方式,可以让我们进行事务操作。所以我们来应用一下。

【完整样例代码:欢迎写邮箱,或者小纸条我,主要十几个包里面的十几个类,复制粘贴确实容易让人没有兴趣看下去。但是都是前面儿的一些知识点的总结。】

 

 serviceImpl 里面添加:

/**
     * 事务这样的操作都写在 业务层 而非 持久化层
     */
    public void tryTransaction(){
        UserPo user1 = getUserById(2);
        UserPo user2 = getUserById(3);
        user1.setUserAge(39);
        userDao.updateUser(user1);
        user2.setUserAge(Integer.parseInt("23"));
        userDao.updateUser(user2);
    }

在beans.xml里面新增事务工厂:

    <!-- 事务管理器 -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <!-- 事务管理器 管理的是哪一个 数据源 -->
        <property name="dataSource" ref="dataSource"></property>
    </bean>
    <!-- 配置事务的代理工厂 -->
    <bean id="proxyFactoryBean" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
        <!-- 给事务代理工厂一个 事务管理器  -->
        <property name="transactionManager" ref="transactionManager"></property>
        <!-- 告诉代理工厂 的代理权限——要管理那些接口 -->
        <property name="proxyInterfaces">
            <list><!-- 列表给值 底层 list和 set 是一个 类型的 -->
                <value>com.letben.service.UserService</value>
            </list>
        </property>
        <!-- 告诉代理工厂要管理的 实现类 -->
        <property name="target" ref="UserService"></property>
        <!-- 告诉事务管理器 要管理的规则 和方法 -->
        <property name="transactionAttributes">
            <props><!-- 参数给值  底层 props 和 map 是一个类型的 -->
                <!-- 下面这个 key对应的是实现类里面的方法。符合通配符的使用规则* -->
                <prop key="tryTransaction">PROPAGATION_REQUIRED</prop><!-- 需要传播 -->
            </props>
        </property>
    </bean>    

这样就完成了 带有工厂的新增事务的配置方式。

当然还有两种。一种是不使用事务工厂的方式,还有一种是自动代理的方式。

 

当然这种织入不仅有日志的添加,事务的处理,还包括权限的检查等等多个方面。所谓切面编程就是在不影响原来事情处理逻辑的基础上,添加内容,让这个流程变得更加完善合情理合逻辑。

 

posted on 2016-02-15 10:54  木鸟飞  阅读(727)  评论(0编辑  收藏  举报

导航