这个命题其实是讲了的,但是之前没有做,发现一些面试会问到,结合自己之前的学习经历。简单把这个问题描述一下。
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>
这样就完成了 带有工厂的新增事务的配置方式。
当然还有两种。一种是不使用事务工厂的方式,还有一种是自动代理的方式。
当然这种织入不仅有日志的添加,事务的处理,还包括权限的检查等等多个方面。所谓切面编程就是在不影响原来事情处理逻辑的基础上,添加内容,让这个流程变得更加完善合情理合逻辑。