springAOP面向切面编程
面向切面编程,以前我们学习的java语言都是面向对象编程或者是C语言的面向过程编程,那么什么是面向切面编程,其实就是编程的中信与操作的核心决定的面向哪里?当然这只是字面意思,通常来讲我们在使用一款网站app的时候,我们从浏览器访问你所需要访问的网站HTTP协议规定注册的网络地址,浏览器收到请求之后会转给服务器,这时服务器将请求转发给后端处理器等实现请求的响应过程,对我们的spring框架开发的软件而言就是:服务器—>controller—>service—>dao—>database这样的一个过程应该就是从上到下的请求过程,响应的过程就是逆向的数据传递过程,那么对于一些边缘性业务,例如不同页面的浏览人数?例如当前网站在线人数?写在哪里比较合适呢?需要考虑的一点就是如果将这些代码放在我们的上到下服务链中的话,会不会产生代码的多次复制?那么在修改的时候,是不是就要先找到那么多代码中的关于这些业务的代码段然后逐一修改呢?可是这样不符合人类的思维习惯,我们更习惯在记笔记的时候将某种标志定义为某种重要程度,而不是胡乱性的涂画,不然到底那几个重要时间一长大家都忘记了,而且定义好的标志,我们可以哪里有用就涂在哪里,当它的重要性降低了我们只需要在记录标志重要性的地方修改它的重要性说明就可以。是不是发现好像我们高中记笔记就是这么做的?这其实也算是解耦合的一种,所谓耦合就是代码重复度高,逻辑代码的嵌入太紧密。内聚则是其反作用的代码性质。我们目前都向往高内聚低耦合的编程风格。说回来,那么我们可不可以像定义标志直接调用在这本书的各处以标注重要性的这种方式实现边缘业务的设置与使用呢?这就是AOP面向切面编程的目的,就是在这条从上到下的链子上划出一个平面,在这个业务逻辑或者功能方法执行的切面上加上你想要的功能,我们可以称之为方法增强。个人理解来说,aop要做的事情其实是把一个从上到下的流水线(如:浏览器页面访问—>Tomcat—>controller—>service—>dao—>数据库)中的某些可以被不同层级去使用的功能或者事务借助xml文件的中间作用,将它们拿出来与主业务分开编程(拿出来被单独编程之后的方法与它原来的主业务属于同一层级但不属于同一主线,所以称为切面,是一个被增强的方法与增强方法组成的面,形象一点就是一把水平切过来的刀,这个描述与c语言里的函数有着相似的性质,我看到了一些面向过程的影子),实现解耦合与功能增强以及可维护性的提升。例如日志,记录某些功能的访问次数,记录订单的取消次数,商品的浏览次数等等都属于面向切面编程的技术。
底层原理浅析
当然我们的AOP毕竟是spring框架下的两大核心技术之一,对于它的底层实现其实和IOC的底层实现有一定的相似,那就是自动化。我们的IOC通过xml文件对java类实现自动实例化,而我们的AOP则是通过xml文件对方法或者是切点进行动态代理。动态代理说白了就是一个方法增强的过程。它具有两种代理方式:基于jdk实现的动态代理与基于cglib实现的动态代理。
先认识一下相关的名词:
Target(目标对象):代理的目标对象。(它的方法称为连接点,其中需要增强的方法称为切点<区别于增强方法>) Proxy (代理):一个类被 AOP 织入增强后,就产生一个结果代理类。 Joinpoint(连接点):所谓连接点是指那些被拦截到的点。在spring中,这些点指的是方法,因为spring只支持方法类型的连接点。 Pointcut(切入点):所谓切入点是指我们要对哪些 Joinpoint 进行拦截的定义。 Advice(通知/ 增强):所谓通知是指拦截到 Joinpoint 之后所要做的事情就是通知。 Aspect(切面):是切入点和通知(引介)的结合。 Weaving(织入):是指把增强应用到目标对象来创建新的代理对象的过程。spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入。
接下来我们来测试两种代理方式:我们的目的是将我们的增强方法嵌入到目标对象的目标方法上,实现在目标方法save运行的前、后、运行期间能够运行我们的增强方法(就像是你进入网站获取服务页面的过程中我就将浏览人数加1了)
目标对象接口与实现类
public interface targetInterface { void save(); } public class target implements targetInterface{ @Override public void save() { System.out.println("目标对象的功能save执行....."); } }
增强方法
public class advice { public void before(){ System.out.println("前置增强"); } public void after(){ System.out.println("后置增强"); } }
基于jdk的动态代理方式
测试代码
public class test { //基于jdk的动态代理,AOP的底层原理之一,为了加深理解,spring会有封装! public static void main(String[] args) { //目标对象的创建,模拟功能 target target = new target(); //获得写好的增强方法,模拟一下日志控制 advice advice = new advice(); //返回值是动态生成的代理对象,用接口接一下形成代理对象pro,jdk的动态代理对象pro与目标对象target属于兄弟关系 targetInterface pro = (targetInterface) Proxy.newProxyInstance( //目标对象的类加载器,java反射中的知识 target.getClass().getClassLoader(), //目标对象相同的字节码对象数组(java的单继承多实现特性,一个对象可能实现多个接口) target.getClass().getInterfaces(), new InvocationHandler() { //这个invoke就是调用代理对象的任何方法都是执行这个invoke方法中 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //切点方法save运行前 advice.before(); Object invoke = method.invoke(target, args); //切点方法save运行后 advice.after(); return invoke; } } ); //调用代理对象的方法 pro.save(); } }
需要注意的是这里的pro代理对象与目标对象之间的逻辑关系,我们在代码中可以发现pro的实例化类型是接口类型,而目标对象也实现了这个接口,所以它们两个不就正符合逻辑上的兄弟关系吗?
基于cglib的动态代理方式
public class test { //基于cglib的动态代理,一样不需要写,spring会对其用一定的封装! public static void main(String[] args) { //目标对象 target target = new target(); //增强对象 advice advice = new advice(); //返回的是动态代理对象,基于cglib生成,与target属于师徒关系(绝对不是继承,用反射实现,没有血缘关系) //1,创建增强器 Enhancer enhancer = new Enhancer(); //2,设置父类(这里注意增强器的反射模板是target.class那就说明在逻辑上其实pro代理对象已经属于下一级) enhancer.setSuperclass(target.class); //3,设置回调函数 enhancer.setCallback(new MethodInterceptor() { @Override public Object intercept(Object proxy, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { //执行前置 advice.before(); //执行目标 Object invoke = method.invoke(target, objects); //执行后置 advice.after(); return invoke; } }); //4,生成代理对象,用目标对象类型接,因为不基于接口,属于师徒关系 target pro = (target) enhancer.create(); pro.save(); } }
这里代码里不难看出pro代理对象与target目标对象之间的关系属于上下级的关系,类似父子关系。
AOP开发
AOP的开发流程其实与上述的测试差不多,只不过动态代理的部分不需要我们去配置,因为spring框架封装了动态代理的两个库,至于如何选择,则是由情况而定。具体的开发方式有两种:xml文件的AOP开发与注解开发
xml文件的AOP开发
首先我们需要了解AOP开发的xml文件写法与标签作用(注意aop命名空间的设置,与context命名空间设置的过程类似)
<!--配置织入:告诉框架目标对象的哪些方法需要哪些增强?(前置增强、后置增强等)--> <aop:config> <!--声明切面--> <aop:aspect ref="aspect"> <!--切点加通知连接起来--> <!--这个标签的含义就是前置增强方法是id为aspect的bean的一个before方法,切点表达式execution(public void com.hlc.aop.target.save())的含义:增强的切入点是公开类target的save方法--> <aop:before method="before" pointcut = "execution(public void com.hlc.aop.target.save())"></aop:before> </aop:aspect> </aop:config> <!--配置目标对象与切面对象bean--> <bean id="target" class="com.hlc.aop.target"/> <bean id="aspect" class="com.hlc.aop.myAspect"/>
这里需要解释的地方在注解里有一定的说明,当然切点表达式的写法还需要我们自己多去学习。
测试代码
目标对象的接口与实现
public interface targetInterface { void save(); void delete(); } public class target implements targetInterface { //我们需要做的就是在这些方法执行时间的前后或者运行期间运行我们自定义的增强方法 @Override public void save() { System.out.println("目标对象的功能save执行....."); } }
创建切面类并在其中写好增强方法(注意回头去看xml文件里的javabean,发现其中的对应关系与逻辑加深使用上的理解)
public class myAspect { public void before(){ System.out.println("前置增强...."); } public void after(){ System.out.println("后置增强...."); } public void bye(){ System.out.println("增强结束...."); } public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { System.out.println("环绕保护进程...."); Object proceed = proceedingJoinPoint.proceed(); System.out.println("运行完毕!保护结束...."); return proceed; } }
测试类(这里集成了Junit包的内容直接使用它的开发手段)
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:spring-aop.xml") public class aopTest { @Autowired private targetInterface target; @Test public void test1(){ target.save(); } }
在xml文件AOP织入的时候我们只配置织入了前置方法,所以测试结果应只显示前置方法的内容与目标对象运行切点方法的内容。
注解开发
通常采用注解加切点表达式的方式为某一个增强方法指定它需要增强的切点形成一个切面,方式比较简单,只需要三步走;第一步写出xml文件中的组件扫描与AOP的注解自动代理(好处是我们不需要手动的将Javabean注入到xml文件中,只需要在java类上加入组件注解让spring框架识别即可)
<!-- aop的自动代理,通常结合组件扫描context一起使用于aop的注解开发--> <aop:aspectj-autoproxy></aop:aspectj-autoproxy> <context:component-scan base-package="com.hlc.aop"/> </beans>
第二步编写目标对象的接口与实现类
public interface targetInterface { void save(); void delete(); } @Component("target") public class target implements targetInterface { @Override public void save() { System.out.println("目标对象的功能save执行....."); } @Override public void delete() { System.out.println("目标对象的功能delete执行....."); } }
第三步编写切面类与增强方法并在增强方法上加入AOP专有注解
//切面类 @Component("myAspect") @Aspect public class myAspect { @Before("execution(public void com.hlc.aop.target.save())") public void before(){ System.out.println("前置增强...."); } @AfterReturning("execution(public void com.hlc.aop.target.delete())") public void after(){ System.out.println("后置增强...."); } @After("execution(public void com.hlc.aop.target.delete())") public void bye(){ System.out.println("增强结束...."); } @Around("execution(public void com.hlc.aop.target.save())") public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { System.out.println("环绕保护进程...."); Object proceed = proceedingJoinPoint.proceed(); System.out.println("运行完毕!保护结束...."); return proceed; } }
补充:
通知的配置语法:@通知注解("切点表达式")
前置通知 @Before 用于配置前置通知。指定增强的方法在切入点方法之前执行 后置通知 @AfterReturning 用于配置后置通知。指定增强的方法在切入点方法之后执行 环绕通知 @Around 用于配置环绕通知。指定增强的方法在切入点方法之前和之后都执行 异常抛出通知 @AfterThrowing 用于配置异常抛出通知。指定增强的方法在出现异常时执行 最终通知 @After 用于配置最终通知。无论增强方式执行是否有异常都会执行
测试类
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:spring-aop.xml") public class aopTest { @Autowired private targetInterface target; @Test public void test1(){ target.save(); } @Test public void test2(){ target.delete(); } }
测试结果:
其实AOP在学生时代的测试用的多的就是数据库的事务控制的配置,我们通常单表操作的时候,比如转账,目前你的账户金额转给我100,那么这一个事务就带出两个数据库操作,一个是你的money-100,我的money+100,但是我们可以试一试,这两个操作是两个方法的调用而已,也就是说如果在两个调用语句之间产生一个异常导致后面没有运行的话,是不是你的money-100成功,而我却没有收到钱?这就是事务存在的作用,事务提交之后才能将两个操作的结果都产生,否则都不成功,事务回滚到最初状态,而spring对于事务也有自己的命名空间与相应的配置方法。
声明式事务控制
事务控制的主要任务就是使我们需要做的数据库工作统一起来,满足:原子性,隔离性,永久性。实现一种看似并发实则串行的数据库操作,同时保护数据的合理性变化,避免脏读,不可重复读等现发生。在代码中我们也可以发现,事务控制的底层也是AOP面向切面编程。
事务的隔离级别 设置隔离级别,可以解决事务并发产生的问题,如脏读、不可重复读和虚读。 ISOLATION_DEFAULT ISOLATION_READ_UNCOMMITTED ISOLATION_READ_COMMITTED ISOLATION_REPEATABLE_READ ISOLATION_SERIALIZABLE 事务的传播行为 REQUIRED:如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。一般的选择(默认值) SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行(没有事务) MANDATORY:使用当前的事务,如果当前没有事务,就抛出异常 REQUERS_NEW:新建事务,如果当前在事务中,把当前事务挂起。 NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起 NEVER:以非事务方式运行,如果当前存在事务,抛出异常 NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行 REQUIRED 类似的操作 超时时间:默认值是-1,没有超时限制。如果有,以秒为单位进行设置 是否只读:建议查询时设置为只读
xml文件声明式事务控制开发
测试代码
UserDaoImpl
public class UserDaoImpl implements UserDao{ @Autowired private JdbcTemplate jdbcTemplate; @Override public void out(String outMan, double money) { jdbcTemplate.update("update account set money=money-? where name=?",money,outMan); } @Override public void in(String inMan, double money) { jdbcTemplate.update("update account set money=money+? where name=?",money,inMan); } }
首先第一步定义需要增强的事务方法
UserServiceImpl
public class UserServiceImpl implements UserService{ private UserDao userDao; public void setUserDao(UserDao userDao) { this.userDao = userDao; } @Override public void transfer(String outMan, String inMan, double money) { //这里分明是两个方法,所以在没有声明式事务控制之前就算在两个方法代码之间加上一个致命的错误,让in无法执行导致转账转出而转入却没有收到。 //增加了事务控制之后,二者就必须同时结束,如果in无法执行,那么out也无法执行。 userDao.out(outMan,money); userDao.in(inMan,money); } }
第二步在xml文件配置事务控制
<!--目标对象内部的方法就是我们的切点--> <bean class="com.hlc.service.UserServiceImpl" id="userService"> <property name="userDao" ref="userDao"/> </bean> <!-- 配置平台事务管理器,需要注入dataSource--> <bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <!--事务的增强,需要引入tx命名空间--> <tx:advice transaction-manager="transactionManager" id="txAdvice"> <tx:attributes> <!--目标对象的一个方法(切点)就是一个事务,每个事务都可以单独设置自己的事务属性,isolation:事务的隔离级别,read-only:读锁,propagation:事务的传播行为的设置 --> <tx:method name="transfer" isolation="REPEATABLE_READ" read-only="false"propagation="REQUIRED"/> </tx:attributes> </tx:advice> <!-- 配置aop织入--> <aop:config> <aop:advisor advice-ref="txAdvice" pointcut="execution(* com.hlc.service.UserServiceImpl.*(..))"/> </aop:config>
之后创建一个测试类并使用注解调用这个javabean并调用增强的transfer方法。
注解开发
观察注解开发与xml文件开发的区别可以更好的理解注解的好处
第一步在xml文件里写上事务控制的注解驱动与组件扫描
<!--组件扫描--> <context:component-scan base-package="com.hlc"/> <!--事务的注解驱动--> <tx:annotation-driven/>
第二步编写UserDao与其实现类
@Repository("userDao") public class UserDaoImpl implements UserDao{ @Autowired private JdbcTemplate jdbcTemplate; @Override public void out(String outMan, double money) { jdbcTemplate.update("update account set money=money-? where name=?",money,outMan); } @Override public void in(String inMan, double money) { jdbcTemplate.update("update account set money=money+? where name=?",money,inMan); } }
第三步编写UserService与其实现类
@Service("userService") //事务控制注解 @Transactional public class UserServiceImpl implements UserService{ private UserDao userDao; public void setUserDao(UserDao userDao) { this.userDao = userDao; } @Override @Transactional //事务控制,这里没有给注解里的属性赋值,但是已经默认方法里的代码功能就是一个整体的事务 public void transfer(String outMan, String inMan, double money) { //这里分明是两个方法,所以在没有声明式事务控制之前就算在两个方法代码之间加上一个致命的错误,让in无法执行导致转账转出而转入却没有收到。 //增加了事务控制之后,二者就必须同时结束,如果in无法执行,那么out也无法执行。 userDao.out(outMan,money); userDao.in(inMan,money); } }
spring框架xml文件常用配置集合
组件扫描
<!-- 组件扫描,去除controller层由mvc自己的配置文件去扫描--> <context:component-scan base-package="扫描的包全限定名"> <context:exclude-filter type="annotation" expression="包中不扫描的层名然后会提示它的框架包全限定名"/> </context:component-scan>
注解驱动
<!--事务的注解驱动--> <tx:annotation-driven/> <!--aop常用的注解自动代理配置--> <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
加载资源文件
<!-- 加载properties文件--> <context:property-placeholder location="classpath:jdbc.properties"/>
配置数据源
<!-- 配置数据源--> <bean class="com.mchange.v2.c3p0.ComboPooledDataSource" id="dataSource"> <property name="driverClass" value="${jdbc.driver}"/> <property name="jdbcUrl" value="${jdbc.url}"/> <property name="user" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean>
建立sql会话工厂
<!-- 配置sql会话工厂--> <bean class="org.mybatis.spring.SqlSessionFactoryBean" id="factory"> <property name="dataSource" ref="dataSource"/> <!--这里的config.xml文件是另一个框架mybatis框架的核心配置文件,通常在整合时配置这个sql会话工厂生成sql会话链接数据库--> <property name="configLocation" value="classpath:config.xml"/> </bean>
AOP织入
<aop:config> <aop:advisor advice-ref="advice" pointcut="execution(* com.hlc.service.UserServiceImpl.*(..))"/> </aop:config>
事务控制
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <tx:advice transaction-manager="transactionManager" id="advice"> <tx:attributes> <tx:method name="findAll" read-only="true"/> </tx:attributes> </tx:advice>
补完啦!跟spring框架彻底说再见,希望我能继续坚持自学的习惯,能够坚持自律!