Spring从认识到细化了解

Posted 知识补全计划

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring从认识到细化了解相关的知识,希望对你有一定的参考价值。

首发日期:2018-08-25
修改日期:
1. 2018-08-29:针对排版问题进行了修改。主要是最前面那部分排版出了问题--一些序号排序失效了。


Spring的介绍

  • Spring框架是目前java应用最广的框架。
  • Spring是一个分层的JavaSE/EE full-stack(一站式) 轻量级开源框架 ,它为每一个层次(表现层、业务层、持久层)都提供了解决方案
    • web层:springMVC【此文不涉及springMVC
    • service层:IoC,DI,AOP
    • dao层:jdbc模板、Hibernate模板
  • spring有几个特点功能【后面将讲这几个功能的好处】:
    • IoC控制反转:将对象的生成权限交给spring,当一个对象需要其他对象时,spring会帮你生成对应的对象。
    • DI:如果使用spring生成的类需要其他类或属性来协助运行,可以使用DI来进行注入。
    • AOP切面编程:通过代理模式,spring可以对某个业务进行强化,而不影响原有的运行。
    • spring提供了与其他框架整合的组件,比如支持Hibernate开发的HibernateTemplate、支持MyBatis开发的SqlSessionTemplate。


基本运行环境搭建

本次环境基于spring4.3.4

1.在官网下载:

下载地址:http://repo.spring.io/libs-release-local/org/springframework/spring/

2.解压压缩包:

  • 重要文件夹:
    • docs:spring的开发规范文档和api文档
    • libs:spring的依赖包、文档(后缀javadoc)和源码(后缀source)
    • schema:spring的配置文件的约束(xsd)。

3.在libs中提取依赖包:【如果你会maven,那么你可以百度一下尝试使用maven来管理依赖

  • spring核心容器包(spring的所有模块都构建在核心容器之上,所以导入核心容器包构建的就是最基础的运行环境)
    • spring-beans-4.3.4.RELEASE.jar:
    • spring-context-4.3.4.RELEASE.jar:
    • spring-core-4.3.4.RELEASE.jar:
    • spring-expression-4.3.4.RELEASE.jar:
  • 日志接口包:commons-logging-1.2.jar【apache提供的,需要自己下载】【必须的】
  • 【如果需要扩展日志功能,才需要导入,这里使用log4j】日志包:log4j-1.2.16.jar【apache提供的,需要自己下载】



ps:上面所引入的依赖包仅能实现基本功能,但比如spring的事务管理、aop这些还需要引入其他的包,这些包将在下面的各个模块中讲。


由于spring很多时候都是提出解决方案,所以下面介绍spring将根据解决方案来讲。

  1. IOC【解决了对象的创建问题】
  2. DI 【解决了属性注入问题】
  3. AOP【解决了业务对象解耦问题】
  4. 模板代码【解决了jdbc代码太繁琐的问题】

IoC

介绍:

  • IoC全称Inversion of Control(控制反转)。
  • IoC就是把对象的生成权限交给spring(原本情况是对象需要我们手动去new,现在声明+配置,spring就可以帮我们把需要的对象自动生成)


示例使用:

1.导入依赖包,IoC只需要基础的依赖包。【这里省去扩展日志依赖包】

2.创建一个用于被spring管理的类(由于仅作演示,所以创建一个简单的实体类):

package work.domain;
public class User {
	private String name;
	private int age;
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public int getAge() {
		return age;
	}
	public void setAge(int age) {
		this.age = age;
	}
	@Override
	public String toString() {
		return "User [name=" + name + ", age=" + age + "]";
	}
}

3.在src目录下创建applicationContext.xml,在applicationContext.xml中把类配置给spring,让spring管理这个类的创建,:

  • applicationContext.xml需要xsd约束,xsd在解压文件夹spring-framework-4.2.4.RELEASE\\docs\\spring-framework-reference\\html\\xsd-configuration.html的the beans schema中。【xsd-configuration.html是spring的配置文件的所有xsd配置的参考文件,可以根据不同配置来查找到不同的xsd配置
  • applicationContext.xml需要放置在classpath可搜索路径下,通常可以放在src目录下。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
	<!-- 配置bean,让spring管理这个类的创建,id用于标识这个类,class是类全限定名 -->
    <bean id="user" class="work.domain.User"></bean>
</beans>

4.创建测试类,获取spring创建的对象:

package work.test;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import work.domain.User;
public class Demo1 {
	@Test
	public void test1() {
		//读取上下文
		ApplicationContext context=new ClassPathXmlApplicationContext("applicationContext.xml");
		//根据id来获取spring实例化对象
		User user = (User) context.getBean("user");
		user.setName("李雷");
		user.setAge(9);
		//能够使用就说明已经被实例化了
		System.out.println(user);
	}
}

上面的示例使用可以说明spring帮我们创建了对象。


使用说明:

  • 如何在applicationContext.xml中配置:

    • xsd约束:

      • xsd在解压文件夹spring-framework-4.2.4.RELEASE\\docs\\spring-framework-reference\\html\\xsd-configuration.htmlthe beans schema中。【xsd-configuration.html是spring的配置文件的所有xsd配置的参考文件,可以根据不同配置来查找到不同的xsd配置】
    • bean

      • 属性

        • id:唯一标志性属性,可以用于标识spring管理的类。【在代码中,我们可以根据这个属性来获取对象】
        • name:不是唯一标志性属性,可以用于标识spring管理的类。虽然可以重复,但为了不引起歧义,通常开发中也不能重复。
        • class:需要实例化的类的路径,指明根据id或name获取类对象的时候,获取的是哪个类的。
      • 示例:

        <bean id="user" name="name_user" class="work.domain.User"></bean>
        
  • 如何在代码中获取spring创建的对象:

    • 先读取上下文,然后利用上下文的getBean方法获取实例化对象:
      • public void test1() {
        		//读取上下文
        		ApplicationContext context=new ClassPathXmlApplicationContext("applicationContext.xml");
        		//获取方法1:根据id来获取spring实例化对象
        		User user = (User) context.getBean("user");
        		//获取方法2:也可以根据name来获取spring对象
        		User user2 = (User) context.getBean("name_user");
        		user.setName("李雷");
        		user.setAge(9);
        		//能够使用就说明已经被实例化了
        		System.out.println(user);
        		System.out.println(user2);
        	}
        

使用注意:

  • 默认情况下,生成的类是单例的,并且spring一开启就会实例化对象。也就是说,默认情况下,在不同代码中多次使用一个类的对象都是同一个对象。【可以通过配置来改变,bean的scope属性可以配置对象是单例的还是多例的】

  • applicationContext.xml的文件名是可以改变的(但默认获取的时候文件名就是这个),如果你的文件名不是这个,创建工厂的时候需要传入其他的文件名:new ClassPathXmlApplicationContext("文件名");

  • spring创建类对象的本质是通过工厂来获取类对象。

    • 老版本的工厂类:BeanFactory
      • BeanFactory:调用getBean的时候,才会生成类的实例。
    • 新版本的工厂类:ApplicationContext【现在都用这个】
      • ApplicationContext:加载配置文件的时候,就会将Spring管理的类都实例化
      • ApplicationContext常见实现类主要有两个:
        • ClassPathXmlApplicationContext :加载类路径下的配置文件
        • FileSystemXmlApplicationContext :加载文件系统下的配置文件
        • 还有一些例如XmlWebApplicationContext,AnnotationConfigApplicationContext,AnnotationConfigWebApplicationContext这些虽然也有用,但这里不涉及,所以不讲。
  • 由于本质是工厂模式,所以它是有利于进行接口化编程的,我们可以在配置文件中很方便地更改它的实现类。比如:

    • 修改前:(获取bean的时候通过id获取,存储的类型是接口,那么可以很方便地更改它的实现类)

      <bean id="calc" class="work.domain.Calc">
      
    • 修改后(只需要修改配置文件,就能给接口提供不同的实现类):

      <!-- 假如功能升级了!给它一个新的实现类。 -->
      <bean id="calc" class="work.domain.SuperCalc">
      

Bean的实例化方式

  • spring创建的对象也可以称为bean

  • spring帮我们实例化的有几种方式,默认是调用无参构造方法来实例化。

    • 默认无参实例化(所以如果没有无参构造函数,将报错):

      • <bean id="user" name="name_user" class="work.domain.User"></bean>
        
    • 注入构造方法参数,带参数实例化:(这个涉及DI内容,具体后面讲)

      •     <bean id="user" name="name_user" class="work.domain.User">
                <!-- 向 User(String name, int age)构造方法注入参数,然后实例化  -->
                <constructor-arg name="name" value="lilei" ></constructor-arg>
                <constructor-arg name="age" value="18" ></constructor-arg>
            </bean>
        
    • 调用静态实例工厂方法得到对象:

      • 静态工厂编写

        public class MyBeanFactory {
            	public static User createUser() {
            		return new User();
            	}
        }
        
      • applicationContext.xml编写

        <!-- class是工厂类路径,factory-method是静态获取对象的方法 -->
             <bean id="factory_user"  class="work.factory.MyBeanFactory" factory-method="createUser"></bean>
        
    • 调用非静态实例工厂方法得到对象:

      • 工厂编写

        public class MyBeanFactory {
            	public  User createU() {
            		return new User();
            	}
        }
        
      • applicationContext.xml编写

          <!-- 先实例化工厂 -->
           <bean id="myBeanFactory" class="work.factory.MyBeanFactory" ></bean>
           <!-- 利用工厂对象来获取对象,factory-bean是上面工厂bean的id,factory-method是获取对象的方法名  -->
           <bean id="user" factory-bean="myBeanFactory"  factory-method="createU"></bean>
        

Bean的作用范围的配置:

在上面说过了,默认情况下,Bean的创建是单例的,但是可以通过bean中的scope属性来配置Bean的创建方式(同时也有作用范围)。

  • scope
    • 取值:
      • singleton:单例模式,只生成一个对象。
      • prototype:多例的,每次通过上下文获取都是获取一个新的对象。
      • request:一次请求创建一个实例。
      • session:一次会话创建一个实例。
    • 示例:
      • <bean id="userAction" class="work.action.UserAction" scope="prototype"></bean>
        

补充:

  • 这里没有讲述基于p名称空间的属性注入和spEL的属性注入【其实挺重要的,但讲细的话太占空间】,有兴趣的可以自查。


DI:

  • 依赖注入。依赖是指某些功能可能依赖某些属性,只有有了对应的属性才能执行功能。当使用了IOC之后,可能需要把某些属性(也包括对象)注入给生成的对象,那么可以使用DI。
    • 比如你利用spring帮你新建了一个类,但是这个类需要另一个类来协助运行(好比CD机需要CD,没有CD的CD机用起来没意思),那么这时候怎么把另一个类传给这个类呢?DI就是解决这类问题的
  • DI是spring的一个很好的属性注入解决方案,如果你不使用DI的话,你可能需要使用接口或者通过继承类来完成属性注入。
    • 其他方案探究:让这个类管理他的协助类,在构造函数中传参进来或者调用某些方法设置属性,但这会造成代码耦合性很高以及难以测试(因为要确保使用协助类之前,协助类要被初始化了)。
  • 使用DI之后,当通过IoC来创建对象的时候(此时,依赖对象和被依赖对象都创建了),IoC也会同时处理依赖管理,帮助我们把协助类注入到目标类中(有构造器注入、set注入等方法)。
  • 像IoC一样,依赖注入也是有利于接口化编程的,当我们注入的属性是一个对象时,如果使用接口名来管理注入,那么我们可以很轻易地更改注入的接口实现类。



属性注入:

属性注入是创建对象的时候,为对象注入属性。

  • 构造方法方式注入:要求类中必须要有与注入参数对应的构造方法

    • 类的编写:

      package work.domain;
      public class User {
      	private String name;
      	private int age;
      	private Account account;
      	public User() {
      		super();
      	}
      	public User(String name, int age) {
      		super();
      		this.name = name;
      		this.age = age;
      	}
      	public String getName() {
      		return name;
      	}
      	public void setName(String name) {
      		this.name = name;
      	}
      	public int getAge() {
      		return age;
      	}
      	public void setAge(int age) {
      		this.age = age;
      	}
      	public Account getAccount() {
      		return account;
      	}
      	public void setAccount(Account account) {
      		this.account = account;
      	}
      }
      
    • applicationContext.xml的编写:

      <bean id="account" class="work.domain.Account"></bean>
          <bean id="user"  class="work.domain.User">
          	 <!--  向 User(String name, int age)构造方法注入参数,然后实例化  -->
          	 <!-- name是属性名,value是属性值,如果属性需要一个类对象,使用ref来注入,值为bean中的id或name -->
          	<constructor-arg name="name" value="lilei" ></constructor-arg>
          	<constructor-arg name="age" value="18" ></constructor-arg>
          	<constructor-arg name="account" ref="account" ></constructor-arg>
       </bean>
      
  • set方法方式,要求类中必须提供注入参数对应的setter方法

    • 类的编写:

      package work.domain;
      public class User {
      	private String name;
      	private int age;
      	private Account account;
      	public User() {
      		super();
      	}
      	public User(String name, int age) {
      		super();
      		this.name = name;
      		this.age = age;
      	}
      	public String getName() {
      		return name;
      	}
      	public void setName(String name) {
      		this.name = name;
      	}
      	public int getAge() {
      		return age;
      	}
      	public void setAge(int age) {
      		this.age = age;
      	}
      	public Account getAccount() {
      		return account;
      	}
      	public void setAccount(Account account) {
      		this.account = account;
      	}
      }
      
    • applicationContext.xml的编写:

      	<bean id="account" class="work.domain.Account"></bean>
          <bean id="user"  class="work.domain.User">
          	 <!--  使用setter来注入参数  -->
          	 <!-- name是属性名,value是属性值,如果属性需要一个类对象,使用ref来注入,值为bean中的id或name -->
          	<property name="name" value="织女"></property>
          	<property name="age" value="1000"></property>
          	<property name="account"  ref="account"></property>
          </bean>
      
  • 上面提到了基本类型属性和对象类型属性的注入,由于集合类型多,所以留到这里再讲:

    • 类的编写(包含了数组,list,set,map):需要提供setter

      • package work.domain;
        
        import java.util.List;
        import java.util.Map;
        import java.util.Set;
        
        public class CollectionBean {
        	private String[] array;
        	private List list;
        	private Set set;
        	private Map map;
        	public void setArray(String[] array) {
        		this.array = array;
        	}
        	public void setList(List list) {
        		this.list = list;
        	}
        	public void setSet(Set set) {
        		this.set = set;
        	}
        	public void setMap(Map map) {
        		this.map = map;
        	}
        	public String[] getArray() {
        		return array;
        	}
        	public List getList() {
        		return list;
        	}
        	public Set getSet() {
        		return set;
        	}
        	public Map getMap() {
        		return map;
        	}
        }
        
    • applicationContext.xml编写

      • <bean id="collecionBean" class="work.domain.CollectionBean" >
           	<!--数组类型的属性注入,name是属性名,数组的元素使用list中的value包裹,也可以使用array中的value包裹;注入的是对象的时候,可以使用ref包裹 -->
           	<property name="array">
           		<list>
           			<value>牛郎</value>
           			<value>织女</value>
           		</list>
           	</property>
           	<!-- List类型的属性注入,name是属性名,数组的元素使用list中的value包裹 -->
           	<property name="list">
           		<list>
           			<value>二郎神</value>
           			<value>华山</value>
           		</list>
           	</property>
           	<!-- Set类型的属性注入,name是属性名,数组的元素使用set中的value包裹 -->
           	<property name="set">
           		<set>
           			<value>孙悟空</value>
           			<value>白骨精</value>
           		</set>
           	</property>
           	<!-- Map类型的属性注入,name是属性名,数组的元素使用entry包裹,如果注入的是对象,那么可以使用key-ref和value-ref来注入 -->
           	<property name="map">
           		<map>
           			<entry key="爱" value="520" ></entry>
           			<entry key="一生一世" value="1314" ></entry>
           		</map>
           	</property>
           </bean>
        
  • 注入其他bean对象:上面的都是一个类对象中的普通属性,但没有说到注入一个其他bean对象时怎么注入。

    •   <bean id="productDao" class="cn.dao.ProductDao"></bean>
        <bean id="productService" class="cn.service.ProductService">
        	<!-- 注入其他bean对象,使用ref,ref是bean的id -->
        	<property name="productDao" ref="productDao"/>
        </bean>
      
  • 属性注入方式还有一种接口注入,接口注入通常使用于资源来自于外界的情况,比如当数据库连接资源来自于外部的时候就可以使用接口注入。这个知识点这里不讲,有兴趣自查。



补充:

  • 还可以从Properties中读取数据来进行注入 ,这里不讲,有兴趣的可以自查【在事务管理中讲述了可以利用一种方式来读取】。
          <context:property-placeholder location="classpath:jdbc.properties" />
          <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
          	<!-- 引用properties文件中的数据 -->
              <property name="driverClass" value="${jdbc.driverClass}" ></property>
              <property name="jdbcUrl" value="${jdbc.url}" ></property>
              <property name="user" value="${jdbc.username}" ></property>
              <property name="password" value="${jdbc.password}" ></property>
          </bean>
    ```
    


IoC的注解方式:

  • 现在已经更趋向于使用注解来配置IOC了。
  • 注解可以减少XML配置。
  • 但XML的配置方式也有好处:
    • 统一管理、方便维护
    • 可以配置第三方组件

依赖包

  • 除了导入基础的核心容器包,还需要spring-aop-4.3.4.RELEASE.jar

示例使用

  • applicationContext.xml配置

    • 导入xsd:

      • 在原来的xsd的基础上,加上context的,context在spring-framework-4.3.4.RELEASE/docs/spring-framework-reference/html/xsd-configuration.html的the context schema中
        <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" xsi:schemaLocation="
                http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> 

        </beans>
  • applicationContext.xml中配置组件扫描:

    • 在xml配置context:component-scan这个标签后,spring可以自动去扫描base-pack下面或者子包下面的java文件,如果扫描到有@Component @Controller@Service等这些注解的类,则把这些类注册为bean 。如果有多个包,可以使用,分隔。
    	<!-- 配置组件扫描,base-package是扫描的包,扫描的包下类的相关注解会把类注册成bean -->
    	<context:component-scan base-package="work.domain"></context:component-scan>
    
  • 给类加上注解@Component("person")

package work.domain;

import org.springframework.stereotype.Component;
//添加注解
@Component("person")  //相当于在applicationContext.xml中配置<bean id="person" class="work.domain.Person" />
public class Person {
	private String name;
	private int age;
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public int getAge() {
		return age;
	}
	public void setAge(int age) {
		this.age = age;
	}
}

上面配置完毕,就可以像XML配置方式一样使用Context上下文获取对象了。



bean配置注解介绍:

  • @Component:修饰一个类,代表这个类的对象交给spring去生成

    • @Component("person") //相当于在applicationContext.xml中配置<bean id="person" class="....." />
    • @Component的衍生注解,下面三个注解与@Component的功能一样,不过多了层次的意义,可以使用下面的来表明是什么层次的bean。【听说后面版本下面的三个注解加了一些功能,没去了解】
      • @Controller:web层
      • @Service:service层
      • @Responsitory:dao层


Bean的作用范围的配置

在类上使用@Scope注解指定Bean的作用范围。

  • @Scope

    • 属性:

      • singleton:单例模式
      • prototype:多例
      • request:存入到request中
      • session:存入到session中
    • 示例:

      • @Component
        @Scope("prototype")
        



DI的注解方式

注解方式的属性注入:

对于类的注解,需要开启IoC上面的组件扫描【组件扫描的时候也会扫描到用于属性注入的注解】;如果仅仅使用注解来进行属性注入,则不需要组件扫描,直接使用下面的。【很多时候,我们可能会使用xml来配置bean,注解来配置属性注入】

<!--只会扫描属性上的注解-->
<context:annotation-config></context:annotation-config>
  • 对于普通类型的属性:

    • @value("属性值")
    • 例如:
  • 对于对象类型的属性:

    • @Autowired:根据类名去进行注入
      • 歧义性问题:部分时候都是采用接口化编程,使得一个接口可能有多个实现类,那么需要使用告诉spring使用哪个类。
        • @Primary用在接口的实现类中,表明使用@Autowired来进行注入的时候,如果有多个实现类,那么优先使用这个。
        • @Qualifier与@Autowired一起使用,@Qualifier("xxx")的时候,代表使用xxx实现类注入。
    • @Resource(name = "....") :根据id去进行注入(对于注解式的,@Component("xxx")中的xxx就是id)【由于@Autowired有歧义问题,所以通常使用@Resource】
  • 对于集合类型的属性,我觉得使用注解就不太清晰了。有兴趣的可以自查,它通常使用@Resource来注入。

  • DI注解的使用:如果有setter方法,就把注解添加到set方法上;如果没有,那么添加到属性上;两者都有的时候建议添加到set方法上。




AOP

  • AOP全称Aspect Oriented Programming (面向切面编程)。
  • AOP可以做到在不改变程序原代码的情况下对程序进行增强。
    • 诸如日志、事务管理和安全这种服务经常用来配合业务,如果把这些服务的代码写在各个业务中,当需要修改,你需要修改很多地方,除此还会可能造成业务逻辑混乱。(业务混乱是指影响了业务实体,增加了不必要的概念,好比某个业务的运行需要先初始化日志对象,这样就影响了本来业务的执行。)
    • AOP通过声明把这类服务应用到它们需要影响的业务中(不需要改变业务代码)。
  • AOP有点类似于拦截器,拦截器是发起请求之后先到拦截器,再到业务逻辑,出去再经过拦截器,拦截器也是脱离了业务逻辑的。
  • 在声明之后,AOP会对目标方法所在的类生成一个代理类对象,这个代理类对象中的目标方法是使用AOP加强过了的。
  • 以事务的理念来谈一下AOP:通常来说,我们的增删改操作需要事务的帮助,我们可能需要在各处代码中都写下开启事务的代码。这样代码可能就很赘余了,而且事务的代码变成了必须品。如果使用了AOP,那么调用方法之前,AOP帮你开启事务,这样你就不必在代码里写下开启事务了。


动态代理技术简单演示

spring AOP的底层实现技术就是动态代理技术。为了帮助了解AOP底层原理,这里基于jdk动态代理对动态代理技术简单演示。【仅作理解,不写太严谨的代码】

1.创建一个接口

      public interface SchoolPerson {
      	public void read();
      	public void save();
      }

2.定义一个实现接口的类

     package work.domain;
     public class Student implements SchoolPerson {
     	public void read() {
     		System.out.println("read");
     	}
     	public void save() {
     		System.out.println("save");
     	}
     }

3.创建一个代理器,实现InvocationHandler接口,创建createProxy方法--负责获取代理对象,创建invoke方法--负责对方法进行增强。

      package work.utils;
      
      import java.lang.reflect.InvocationHandler;
      import java.lang.reflect.Method;
      import java.lang.reflect.Proxy;
      
      import work.domain.SchoolPerson;
      
      public class JDKProxy implements InvocationHandler {
      	//被代理的对象
      	private SchoolPerson sp;
      	//把对象传给代理器,让它生成一个代理对象
      	public JDKProxy(SchoolPerson sp) {
      		this.sp = sp;
      	}
      	//生成代理对象,并绑定代理方法
      	public SchoolPerson createProxy() {
      		SchoolPerson proxyStu = (SchoolPerson) Proxy.newProxyInstance(sp.getClass().getClassLoader(), sp.getClass().getInterfaces(), this);
      		return proxyStu;
      	}
      	@Override
      	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      		if(method.getName().equals("save")) {
      			System.out.println("增强功能执行---。");
      			return  method.invoke(sp, args);
      		}
      		return method.invoke(sp, args);
      	}
      }

4.测试:

      package work.test;
      import org.junit.Test;
      import work.domain.SchoolPerson;
      import work.domain.Student;
      import work.utils.JDKProxy;
      
      public class Demo2 {
      	@Test
      	public void test1() {
      		SchoolPerson sp=new Student();
      		JDKProxy proxy = new JDKProxy(sp);
      		SchoolPerson createProxy = proxy.createProxy();
              //调用方法,测试是否被增强
      		createProxy.save();
      		/*测试结果:
      		 * 增强功能执行---。
      			save
      		 */
      	}
      }

上述例子可以看出,student对象原有的save方法被增强了,而且我们并没有修改过student的代码。



AOP相关术语:

  • Aspect:切面,切面是指贯穿多个业务的环境。比如拦截器就贯穿了业务逻辑。

  • Advice:通知、增强。是指增强的方法,根据逻辑和顺序可以区分成多个通知类型(下面讲)

  • Joinpoint:连接点,可拦截点,判断一个点是否是切点。

  • Pointcut:切点,被切面拦截的点就是一个拦截点。

  • Target:目标,被增强的对象

  • Weaving:织入,生成代理对象的过程

  • Proxy:代理对象



通知类型:

  • 前置通知before:在目标方法之前执行通知里面的操作

  • 正常返回通知after-returning:目标方法正常执行完成之后,进行通知里面的操作

  • 环绕通知around:目标方法执行之前和目标方法执行完成之后,进行通知里面的操作

  • 异常抛出通知after-throw:发生异常时,进行通知里面的操作

  • 后置(最终)通知after:运行完成时,无论是否发生异常,都进行通知里面的操作

  • 五种通知的执行顺序为: 前置通知→环绕通知→后置通知→正常返回通知/异常抛出通知



使用示例:

  • 导入依赖包:

    • 基本包:
    • AOP依赖包
      • 两个spring自带的:
        • spring-aop-4.3.4.RELEASE.jar
        • spring-aspects-4.3.4.RELEASE.jar
      • 三个需要下载的:
        • aopalliance-1.0.jar
        • aspectjrt-1.7.2.jar
        • aspectjweaver-1.7.2.jar
  • 编写一个接口:

    • package work.dao;
      public interface StudentDao {
      	public void read();
      	public void save();
      }
      
  • 编写一个接口的实现类:

    • package work.dao.impl;
      import work.dao.StudentDao;
      public class StudenDaoImpl implements StudentDao {
      	@Override
      	public void read() {
      		System.out.println("read");
      	}
      	@Override
      	public void save() {
      		System.out.println("save");
      	}
      }
      
  • 编写一个切面类,用于调用切面类的方法对目标对象增强:

    • package work.utils;
      
      import org.aspectj.lang.ProceedingJoinPoint;
      
      public class MyAspect {
      	public void before() {
      		System.out.println("before。。。");
      	}
      	public void after() {
      		System.out.println("after。。。");
      	}
      	public void afterReturn() {
      		System.out.println("afterReturn。。。");
      	}
      	public void around(ProceedingJoinPoint pj) throws Throwable {
      		System.out.println("around in。。。");
      		pj.proceed();
      		System.out.println("around out。。。");
      	}
      	public void afterThrow() {
      		System.out.println("afterThrow。。。");
      	}
      }
      
  • 配置applicationContext.xml文件

    • 导入xsd,他在xsd-configuration.html的the aop schema中(如果你有其他功能,注意进行保留,在原来的基础上增加aop的xsd)

      • <?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.xsd">
        </beans>
        
    • 把接口实现类交给Spring去管理

      • <!-- 把目标实现类交给spring管理 -->
           <bean id="studentDao" class="work.dao.impl.StudenDaoImpl"></bean>
        
  • 把切面交给Spring去管理

    • <!-- 把切面类交给spring管理 -->
        <bean id="myAspect" class="work.utils.MyAspect"></bean>
      
  • 配置AOP

    •     <!-- 配置AOP -->
          <aop:config>
              	<!-- 配置切点,配置哪些类的哪些方法需要增强 -->
              	<aop:pointcut expression="execution(* work.dao.impl.StudenDaoImpl.*(..))" id="pointcut1"/>
              	<!-- 配置切面,配置通知与切点的关系 -->
              	<aop:aspect ref="myAspect">
              		<aop:before method="before" pointcut-ref="pointcut1"/>
              		<aop:after method="after"  pointcut-ref="pointcut1" />
              		<aop:after-returning method="afterReturn"  pointcut-ref="pointcut1"  />
              		<aop:around method="around"  pointcut-ref="pointcut1" />
              	</aop:aspect>
          </aop:config>
      
  • 编写测试方法:

    • //测试AOP
      	@Test
      	public void test2() {
      		ApplicationContext context=new ClassPathXmlApplicationContext("applicationContext.xml");
      		StudentDao studentDao = (StudentDao) context.getBean("studentDao");
      		studentDao.read();
      	}
      


使用详解:

AOP配置结构

  •     <!-- aop:config是配置AOP的顶层标签 -->
        <aop:config>
            	<!-- aop:pointcut配置切点,配置哪些类的哪些方法需要增强 -->
            	<aop:pointcut expression="..." id="..."/>
            	<!-- aop:aspect配置切面 -->
            	<aop:aspect ref="myAspect"><!-- ref引用的是切面类的bean -->
                    <!--配置对于切点怎么增强 -->
            		<aop:before method="..." pointcut-ref="..."/>
            		<aop:after method="..."  pointcut-ref="..." />
            		<aop:after-returning method="..."  pointcut-ref="..."  />
            		<aop:around method="..."  pointcut-ref="..." />
            	</aop:aspect>
        </aop:config>
    


切点:

在xml中,用<aop:pointcut expression="..." id="..."/>来声明一个切点,使得增强可以针对一个切点来进行。其中id是用于标识一个切点的;expression是用来表示切点的位置的,表示增强用于哪个类的哪个方法上。

  • expression的写法
    • 语法格式:expression="execution(访问修饰符 返回类型 包名.类名.方法名(参数) )"
    • execution()代表方法执行时触发
    • 访问修饰符(public之类的)是可选的。
    • 返回类型是必需的,可以使用*来代表任意返回值类型的
    • 包名.类名 ,可以使用*来代表任意包任意类,但注意要符合层次比如work.demo.dao可以用*.*.*代替
    • 方法名,可以使用*来代表任意方法
    • 参数中填写的是参数类型,(..)表示任意个数任意类型参数,(.)表示有一个参数 ,(*,String)表示第一个参数为任意,第二个为String类型
  • expression的示例:
    • public void com.domain.Custom.save(..)
    • * *.*.*.save(..)
  • expression的语法类似于正则表达式,这里仅仅讲了一些基础的,足够你基本使用的。如果想了解更多,可以自查。


切面:

在xml中,用<aop:aspect ref="..."></aop:aspect>来声明一个切面,声明切面怎么对切点增强。

  • aop:aspect 的属性:
    • ref用来引用spring管理的切面类的bean。
    • order:用与多个切面处理同一个切点的时候,决定它们之间的顺序。没有的时候,多个切面处理一个切点的时候,顺序是乱的,如果想有顺序的,需要使用order。
  • 通知类型【aop:aspect 下使用各种通知类型来定义什么时候对切点进行增强】
    • 前置通知:<aop:before method="调用的方法" pointcut-ref="切点id"/>
    • 后置通知:<aop:after method="调用的方法" pointcut-ref="切点id" />
    • 环绕通知:<aop:around method="调用的方法" pointcut-ref="切点id" />
    • 返回通知:<aop:after-returning method="调用的方法" pointcut-ref="切点id" />
    • 异常抛出通知:<aop:after-throwing method="调用的方法" pointcut-ref="切点id" />


切面类:

切面类是用来对切点增强的,在xml中配置的通知类型的method就是切面类中的方法名。

通知类型有不同的作用,所以切面类也需要规范写法。

  • 前置通知before可以用来获取切点信息,在方法中需要定义一个形参JoinPoint pj,这个形参会自动传入。

    • public void before(JoinPoint jp) {
      		System.out.println("before。。。"+jp);
      	}
      
  • 正常通知after-returning既可以用来获取切点信息,也可以获取目标方法的返回值。如果在目标方法中进行了返回,可以在xml中使用returning属性来指明存储到哪个变量中。

    • xml中:<aop:after-returning method="afterReturn" pointcut-ref="pointcut1" returning="result" />【returning中的值是通知中的形参的名字。通知中通过形参来获取目标方法的返回值】
    • 当获取返回值的时候要注意环绕通知的使用,获取返回值需要在环绕通知中使用return pj.proceed();
  • 环绕通知可以用来拦截方法,所以它需要“放行”才能正常环绕,在方法中需要定义一个形参ProceedingJoinPoint pj(会自动传入值),然后pj.proceed()就可以放行了。

    • public void around(ProceedingJoinPoint pj) throws Throwable {
      		System.out.println("around in。。。");
      		Object obj=pj.proceed();
      		System.out.println("around out。。。");
              return obj;
      	}
      
  • 异常抛出通知可以获取目标方法的异常信息

补充

  • 还可以对通知类型传入参数,这样在切面的类的方法中可以根据参数来执行。这里不讲,有兴趣自查。

AOP的注解方式:

  • AOP的注解方式不需要引入额外的其他依赖包,只需要引入前面的核心容器包和aop包即可。

使用示例

开启自动代理

  • 在xml中开启AOP自动代理【其实也可以全注解配置--可以创建一个java类来进行配置,但这里不讲。】
    • <aop:aspectj-autoproxy></aop:aspectj-autoproxy><!-- 这个标签与bean同级,不要配错了。 -->
      

创建切面并定义通知

使用@Aspect来注解一个类,Spring就会认为这个类是一个切面类。使用@Before来注解一个方法,Spring就会把这个方法当成切面类的前置通知。

package work.utils;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
//注解,标明这个是一个切面类
@Aspect
public class MyAspect {
	//前置通知
	@Before(value="execution(* work.dao.impl.StudenDaoImpl.*(..))")
	public void before() {
		System.out.println("before。。。");
	}
}

这样之后,获取的对象就是被AOP前置增强了的。



使用详解:

通知类型:

  • 前置通知:@Before
  • 正常返回通知:@AfterReturning
  • 环绕通知:@Around
  • 异常抛出通知:@AfterThrowing
  • 后置通知:@After


### 切点:
  • 当在通知中同时声明一个切点的时候,使用的是切点表达式,可以参考上面XML中的切点表达式的写法。

  • 除了切点表达式,还可以使用注解声明切点,然后在通知是使用切点名即可。

    • 使用@Pointcut注解来声明切点,它使用在方法上,这个方法是没有意义的,可以为空方法,方法名相当于切点的ID。Pointcut的值为切点表达式。可以在通知中使用 类名.方法名() 来指向一个切点。

      • package work.utils;
        
        import org.aspectj.lang.annotation.Aspect;
        import org.aspectj.lang.annotation.Before;
        import org.aspectj.lang.annotation.Pointcut;
        //注解,标明这个是一个切面类
        @Aspect
        public class MyAspect {
        	@Pointcut(value="execution(* work.dao.impl.StudenDaoImpl.*(..))")
        	public void pointcut1() {}
        //	@Before(value="execution(* work.dao.impl.StudenDaoImpl.*(..))")
        	@Before(value="MyAspect.pointcut1()")
        	public void before() {
        		System.out.println("before。。。");
        	}
        }
        


补充:

  • 多个切面对一个切点增强时,如果想要按顺序处理,可以在切面类上使用注解@Order,Order的值影响它们的顺序。

Spring与数据库:

  • 对于非框架式的JDBC数据库编程,Spring提供jdbc模板来简化jdbc的使用
  • 对于Hibernate框架,Spring提供了Hibernate模板来简化Spring整合Hibernate时对Hibernate的操作。
  • 对于MyBatis框架,Spring没有提供很好的MyBatis模板,通常使用MyBatis社区开发了MyBatis-Spring整合包,其中SqlSessionTemplate就相当于MyBatis模板。
  • 对于Hibernate和MyBatis框架,这里不涉及框架整合知识,所以这里只介绍一下jdbc模板。[整合框架的内容迟一点写]


JDBC模板使用示例

  • JDBC模板可以说是Spring进行jdbc的封装,它的使用就像DbUtils。

1. 引入依赖包:

  • spring核心容器基本包
  • 数据库驱动包:mysql-connector-java-5.1.7-bin.jar
  • Spring的JDBC包:spring-jdbc-4.3.4.RELEASE.jar
  • Spring的事务包(需要事务时才导入):spring-tx-4.3.4.RELEASE.jar

2. 创建数据连接资源(这里以简单数据库连接池为例)

		SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
		dataSource.setDriverClass(com.mysql.jdbc.Driver.class);
		dataSource.setUrl("jdbc:mysql://localhost:3306/spring");
		dataSource.setUsername("root");
		dataSource.setPassword("123456");

3. 获取Jdbc模板

JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);

4.使用jdbc模板操作数据库

这里以保存数据为例:

@Test
	public void test3() {
		//创建简单连接池
		SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
		dataSource.setDriverClass(com.mysql.jdbc.Driver.class);
		dataSource.setUrl("jdbc:mysql://localhost:3306/spring");
		dataSource.setUsername("root");
		dataSource.setPassword("123456");
		//获取jdbc模板
		JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
		//操作数据库
		jdbcTemplate.update("insert into userinfo values(null,?,?) ","李雷",20);
	}


使用详解

jdbc模板的使用

  • 新增、修改、删除:jdbc模板的的增删改主要是通过update方法来与数据库交互的。

    • public void test4() {
      		//获取jdbc模板
      		ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
      		JdbcTemplate jdbcTemplate = (JdbcTemplate) context.getBean("jdbcTempldate");
      		//操作数据库
      		jdbcTemplate.update("insert into userinfo values(null,?,?) ","韩雷",20);
      		jdbcTemplate.update("delete from userinfo where id=3");
      		jdbcTemplate.update("update userinfo set name=\'地雷\' where id=?", 4);
      	}
      


  • 查询:

    • 查询主要依靠query方法和queryForObject方法,他们的使用与DbUtils中的查询使用类似,都是需要传入一个结果集处理对象

    • 下面只给出一个小例子(这个例子是使用了spring注入生成template的结果,配置方法在下下份代码中),基本跟DbUtils的用法没什么区别,所以你也可以自己把它封装到对象中

      	@Test
      	public void test5() {
      		//获取jdbc模板
      		ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
      		JdbcTemplate jdbcTemplate = (JdbcTemplate) context.getBean("jdbcTempldate");
      		//操作数据库
      		jdbcTemplate.query("select * from userinfo", new RowMapper() {
      			@Override
      			public Object mapRow(ResultSet rs, int rowNum) throws SQLException {
      				String name=rs.getString("name");
      				int age=rs.getInt("age");
      				System.out.println(name+":"+age);
      				return null;
      			}
      		});
      	}
      
    • queryForObject可以用来获取单个数据,也可以用来像query一样用结果集处理对象来处理数据

      @Test
      	public void test5() {
      		//获取jdbc模板
      		ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
      		JdbcTemplate jdbcTemplate = (JdbcTemplate) context.getBean("jdbcTempldate");
      		
      		Long num = jdbcTemplate.queryForObject("select count(*) from userinfo",Long.class);
      		System.out.println(num);
      	}
      
  • 上面讲了jdbc模板的使用涉及spring管理jdbc模板,了jdbc模板也可以交给spring去管理。

    • <!-- class里面写dbcp的类,使用dataSource获取的时候就是dbcp连接池对象 -->
      	<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
      		<!-- 由于里面有很多setter,可以直接属性注入 -->
      		<property name="driverClassName" value="com.mysql.jdbc.Driver" ></property>
      		<property name="url" value="jdbc:mysql://localhost:3306/spring" ></property>
      		<property name="username" value="root" ></property>
      		<property name="password" value="123456" ></property>
      	</bean>
        
          <bean id="jdbcTempldate" class="org.springframework.jdbc.core.JdbcTemplate">
          	    <!-- jdbc模板需要数据库连接池,这里注入一个dbcp的连接池对象 -->
              	<property name="dataSource" ref="dataSource"></property>
          </bean>
      


配置数据库资源

如果你之前学过第三方数据库连接池的配置的话,你应该了解到他们都是DataSource接口的实现类,再想想上面的IoC,所以其实Spring中配置数据库资源就是将连接池类交给Spring去管理。(如果是在代码中创建数据库连接资源的话,像普通的编写即可,跟Spring没关系)

  • 配置DBCP连接池:
    • 引入dbcp依赖包:
      • commons-dbcp-1.4.jar
      • commons-pool-1.5.6.jar
    • 配置DBCP:
      • 让spring管理dataSource对象。
<!-- class里面写dbcp的类,使用dataSource获取的时候就是dbcp连接池对象 -->
	<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
		<!-- 由于里面有很多setter,可以直接属性注入 -->
		<property name="driverClassName" value="com.mysql.jdbc.Driver" ></property>
		<property name="url" value="jdbc:mysql://localhost:3306/spring" ></property>
		<property name="username" value="root" ></property>
		<property name="password" value="123456" ></property>
	</bean>
  • 配置C3P0连接池:
    • 引入依赖包:
      • c3p0-0.9.1.2.jar
    • 配置连接池,把连接池交给spring管理:
<!-- class里面写c3p0的类,使用dataSource获取的时候就是c3p0连接池对象 -->
	<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
		<!-- 由于里面有很多setter,可以直接属性注入 -->
		<property name="driverClass" value="com.mysql.jdbc.Driver" ></property>
		<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/spring" ></property>
		<property name="user" value="root" ></property>
		<property name="password" value="123456" ></property>
	</bean>

引入外部配置文件:

在上面的连接池配置过程中,你可能会发现一个问题,以往会使用配置文件来解决在代码中把参数写成固定值的问题,在上面中也是使用了固定值,那么怎么使用外部配置文件来解决这个问题呢?

1.引入Property配置文件:【注意一下下面标签是以context开头的,所以它需要什么xsd,你懂的】

      <context:property-placeholder location="classpath:jdbc.properties" />

2.根据配置文件中的key【要求key是点分的多单词,测试过单个单词无法获取】,引用配置文件中的参数:

      <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
      		<property name="driverClass" value="${jdbc.driverClass}" ></property>
      		<property name="jdbcUrl" value="${jdbc.url}" ></property>
      		<property name="user" value="${jdbc.username}" ></property>
      		<property name="password" value="${jdbc.password}" ></property>
      	</bean>

给一下配置文件的写法:

jdbc.driverClass=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/spring
jdbc.username=root
jdbc.password=123456



事务管理

  • 在上面中讲述了Spring对数据库操作的支持,但没有涉及事务管理,并且实质上jdbc模板是没有自带事务管理的。
  • Spring对事务的管理是通过PlatformTransactionManager管理的。

Spring用于管理事务的几个类

  • PlatformTransactionManager:一个接口,spring用于管理事务

    • DataSourceTransactionManager:PlatformTransactionManager的实现类,可用于jdbc模板和MyBatis框架
    • HibernateTransactionManager:PlatformTransactionManager的实现类,主要针对Hibernate框架
  • TransactionDefinition:事务定义信息,包含了一些关于事务管理的变量(隔离级别、传播行为等等),spring会把方法\\类中配置的事务相关属性装载到TransactionDefinition中。

  • TransactionStatus:事务的状态

  • 平台事务管理器PlatformTransactionManager会根据事务定义信息TransactionDefinition的信息来进行事务管理。事务管理中产生的状态信息存储到TransactionStatus中。



声明式事务管理

xml方式【XML的方式有很多种,例如拦截器方式、通知方式,这里介绍通知方式】:

1.导入事务管理的xsd:

      <?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"
              xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="
              http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
              http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
              http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> 
      
      </beans>

2.配置事务管理器

     <!--使用事务管理器,必须注入一个dataSource -->
     <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
     	<property name="dataSource" ref="dataSource" />
     </bean>

3.配置事务的增强,把事务管理器当成切面类来对方法增强,相当于给方法配置一个事务管理增强。

  •  <!-- 把spring管理的transactionManager赋给transaction-manager属性 -->
         <tx:advice id="txAdvice" transaction-manager="transactionManager" >
    		<tx:attributes>
    			<!-- name写要进行事务管理的方法名 -->
    	        <tx:method name="transfer"  />
    	    </tx:attributes>
    	 </tx:advice>
    

4.AOP配置,把上面的事务增强配置给哪个切点,这些切点就是需要事务管理的地方:

	 <aop:config>
	 	<!-- 这里的切点注意要写把多个数据库操作集合起来的方法 -->
	 	<aop:pointcut expression="execution(* work.service.impl.AccountServiceImpl.*(..))" id="pointcut1"/>
	 	<aop:advisor advice-ref="txAdvice" pointcut-ref="pointcut1"/>
	 </aop:config>

5.测试类的编写:

注解式

1.导入事务管理tx和aop的xsd

2.配置事务管理器

  • <!--使用事务管理器,必须注入一个dataSource -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    	<property name="dataSource" ref="dataSource" />
    </bean>
    

3.开启注解式事务

  • <tx:annotation-driven transaction-manager="transactionManager"/>
    

4.在业务层增加@Transactional注解:

  • 使用在类上,代表类的所有方法都使用事务管理
  • 也可以仅使用在某个方法上。


使用详解: