Spring

36 阅读44分钟

Spring教程

Spring简介

Spring是一个轻量级Java开发框架,由Rod Johnson创建,目的是为了解决企业级应用开发的业务逻辑层和其他各层的耦合问题。它是一个JavaSE/JavaEE分层的full-stack(一站式)轻量级开源框架,为开发Java应用程序提供全面的基础架构支持。

Spring负责基础架构,因此Java开发者可以专注于应用程序的开发。

Spring的优点

  1. 方便解耦,简化开发:Spring就是一个大工厂,可以将所有对象创建和依赖的关系维护,交给Spring管理。
  2. AOP编程的支持:Spring提供面向切面编程,可以方便的实现对程序进行权限拦截、运行监控等功能。
  3. 声明式事务的支持:只需要通过配置就可以完成对事务的管理,而无需手动编程。
  4. 方便程序的测试:Spring对Junit4支持,可以通过注解方便的测试Spring程序。
  5. 方便集成各种优秀框架:Spring不排斥各种优秀的开源框架,其内部提供了对各种优秀框架的直接支持(如:Struts、Hibernate、MyBatis等)。
  6. 降低JavaEE API的使用难度:Spring对JavaEE开发中非常难用的一些API(JDBC、JavaMail、远程调用等),都提供了封装,使这些API应用难度大大降低。

Spring框架可以说是当前Java世界中最为成功的框架,在企业实际应用中,大部分的企业架构都基于Spring框架。Spring的成功来自于理念,而不是技术,最核心的理念是控制反转(IOC/DI)面向切面编程(AOP) ,以及声明式事务。其中IOC是spring的基础,AOP则是其重要的功能,最为典型的当属数据库事务的使用。

spring框架已经融入了J2EE开发的各个领域,不论是数据访问层,还是控制层,又或是表现层,全都可以看到spring的身影。

Spring体系结构

Spring框架至今已集成了20多个模块,这些模块分布在以下模块中:

  • 核心容器(Core Container)
  • 数据访问/集成(Data Access/Integration)层
  • Web层
  • AOP(Aspect Oriented Programming)模块
  • 植入(Instrumentation)模块
  • 消息传输(Messaging)
  • 测试(Test)模块

核心容器

Spring的核心容器是其他模块建立的基础,有Spring-core、Spring-beans、Spring-context、Spring-context-support和Spring-expression(Spring表达式语言)等模块组成。

  • Spring-core模块:提供了框架的基本组成部分,包括控制反转(Inversion of Control,IOC)和依赖注入(Dependency Injection,DI)功能。
  • Spring-beans模块:提供了BeanFactory,是工厂模式的一个经典实现,Spring将管理对象称为Bean。
  • Spring-context模块:建立在Core和Beans模块的基础之上,提供一个框架式的对象访问方式,是访问定义和配置的任何对象的媒介。ApplicationContext接口是Context模块的焦点。
  • Spring-context-support模块:支持整合第三方库到Spring应用程序上下文,特别是用于高速缓存(EhCache、JCache)和任务调度(CommonJ、Quartz)的支持。
  • Spring-expression模块:提供了强大的表达式语言去支持运行时查询和操作对象图。这是对JSP2.1规范中规定的统一表达式语言(Unified EL)的扩展。该语言支持设置和获取属性值、属性分配、方法调用、访问数组、集合和索引器的内容、逻辑和算术运算、变量命名以及从Spring的IOC容器中以名称检索对象。它还支持列表投影、选择以及常用的列表聚合。

AOP和Instrumentation

  • Spring-aop模块:提供了一个符合AOP要求的面向切面的编程实现,允许定义方法拦截器和切入点,将代码按照功能进行分离,以便干净地解耦。
  • Spring-aspects模块:提供了与AspectJ的集成功能,AspectJ是一个功能强大且成熟的AOP框架。
  • Spring-instrument模块:提供了类植入(Instrumentation)支持和类加载器的实现,可以在特定的应用服务器中使用。

消息

Spring4.0以后新增了消息(Spring-messaging)模块,该模块提供了对消息传递体系结构和协议的支持。

数据访问/集成

数据访问/集成层由JDBC、ORM、OXM、JMS和事务模块组成。

  • Spring-jdbc模块:提供了一个JDBC的抽象层,消除了烦琐的JDBC编码和数据库厂商特有的错误代码解析。
  • Spring-orm模块:为流行的对象关系映射(Object-Relational Mapping)API提供集成层,包括JPA和Hibernate。使用Spring-orm模块可以将这些O/R映射框架与Spring提供的所有其他功能结合使用,例如声明式事务管理功能。
  • Spring-oxm模块:提供了一个支持对象/XML映射的抽象层实现,例如JAXB、Castor、JiBX和XStream。
  • Spring-jms模块(Java Messaging Service):指Java消息传递服务,包含用于生产和使用消息的功能。自Spring4.1以后,提供了与Spring-messaging模块的集成。
  • Spring-tx模块(事务模块):支持用于实现特殊接口和所有POJO(普通Java对象)类的编程和声明式事务管理。

Web

Web层由Spring-web、Spring-webmvc、Spring-websocket和Portlet模块组成。

  • Spring-web模块:提供了基本的Web开发集成功能,例如多文件上传功能、使用Servlet监听器初始化一个IOC容器以及Web应用上下文。
  • Spring-webmvc模块:也称为Web-Servlet模块,包含用于web应用程序的Spring MVC和REST Web Services实现。Spring MVC框架提供了领域模型代码和Web表单之间的清晰分离,并与Spring Framework的所有其他功能集成。
  • Spring-websocket模块:Spring4.0以后新增的模块,它提供了WebSocket和SocketJS的实现。
  • Portlet模块:类似于Servlet模块的功能,提供了Portlet环境下的MVC实现。

测试

Spring-test模块支持使用JUnit或TestNG对Spring组件进行单元测试和集成测试。

spring环境搭建

使用Maven搭建spring环境,在pom.xml中进行以下依赖即可:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-core</artifactId>
    <version>5.2.2.RELEASE</version>
</dependency>

不使用Maven,则需要下载spring的压缩包,并解压将对应jar包导入项目。

下载: repo.spring.io/list/libs-r… ,选择spring-版本号-release-dist.zip下载即可。

将下载的压缩包解压后,可以从libs目录中找到spring各个模块的jar。

spring的不同模块具备不同的功能,因此,具体的spring环境搭建,应该根据当前开发的应用使用到的功能来决定,将某个模块的jar包导入项目即可使用该模块对应的功能。

IoC/DI:控制反转/依赖注入

控制反转是软件设计大师 Martin Fowler在 2004 年发表的《Inversion of Control Containers and the Dependency Injection pattern》提出的。这篇文章系统阐述了控制反转的思想,提出了控制反转有依赖查找和依赖注入实现方式。

控制反转是一种通过描述(XML或注解)并通过第三方去产生或获取特定对象的方式。使用控制反转带来的最大好处就是降低对象之间的耦合。

程序中对象的产生是基于IoC容器,而不是开发者主动的行为。开发者主动创建的模式,责任归于开发者,在使用IoC容器被动创建的模式下,责任归于IoC容器。基于这样的被动形式,我们就说对象被控制反转了。

spring支持XML和注解两种方式实现IoC。

自定义IoC框架理解IoC/DI

需求:模拟通过配置文件实现IoC/DI、通过注解实现IoC/DI。

# 1.通过xml配置文件实现自定义IoC框架
//模拟三层架构
//dao
public class UserDao {
    public void dao(){
        System.out.println("dao层方法执行!!!");
    }
}
//service
public class UserService {
    private UserDao userDao=new UserDao();
    public void service(){
        System.out.println("service层方法执行!!!");
        userDao.dao();
    }
}
//servlet
public class UserServlet {
    public static void main(String[] args) {
        UserService service = new UserService();
        userService.service();
    }
}
# 2.理解IoC框架要实现的功能
# 3.创建对应的类
//PropertyDifinition
public class PropertyDifinition {
    private String name;
    private String ref;
    //setter/getter......
}
​
//BeanDifinition
public class BeanDifinition {
    private String id;
    private String className;
    private List<PropertyDifinition> propertyDifinitions;
    //setter/getter......
}
​
//ApplicationContext:容器类型有多种,通过读取数据方式不同而不同,此处需要使用两种:xml或注解
public interface ApplicationContext {
    void addBean(String id,Object object);//将实例存入容器
    Object getBean(String id);//根据实例唯一标识从容器中取出实例
}
​
//ClasspathXmlApplicatonContext:先实现读取xml文件完成IoC
public class ClasspathXmlApplicationContext implements ApplicationContext{
    private Map<String,Object> beans=new HashMap<>();//容器
​
    public ClasspathXmlApplicationContext() {
        BeanFactory beanFactory = new BeanFactory(this);//将容器传入BeanFactory,方便调用
        beanFactory.initBean();//初始化bean--控制反转
        beanFactory.dependencyInjection();//依赖注入
    }
    public void addBean(String id,Object object){
        beans.put(id,object);//向容器中存放实例
    }
​
    public Object getBean(String id){
        return beans.get(id);//从容器中取出实例
    }
}
​
​
//BeanFactory
public class BeanFactory {
    private List<BeanDifinition> beanDifinitions=new ArrayList<>();//存放所有的<bean>标签信息
    private ApplicationContext ApplicationContext;//
​
    /**
     * 在构造方法中读取配置文件,将配置文件内容存入beanDifinitions
     * IoC容器分为两种类型:一种通过读取配置文件构建,一种通过读取类的注解构建
     * 使用接口类型实现多态
     * 需要注意的是:此处只为简单实现以理解IoC原理,没有去考虑xml与注解混用的情况
     */
    public BeanFactory(ApplicationContext ApplicationContext) {
        this.applicationContext=applicationContext;
        String path = this.getClass().getResource("/").getPath();//获取项目根目录
        //项目根目录以“/”开头时,可使用subString()用于去除,否则不必执行截取操作,但replace()操作必须有
        path=path.substring(1).replace("/",File.separator);
        //如果传入的容器是读取xml配置时,读取配置文件信息
        if (applicationContext instanceof ClasspathXmlApplicationContext) {
            try {
                SAXReader saxReader = new SAXReader();
                Document document = saxReader.read(path+"applicationContext.xml");
                Element root = document.getRootElement();
                List<Element> beanElements = root.elements("bean");
                //遍历所有bean标签,将bean标签信息存入BeanDifinition
                beanElements.forEach(element -> {
                    BeanDifinition beanDifinition = new BeanDifinition();
                    String id = element.attributeValue("id");
                    String className = element.attributeValue("class");
                    beanDifinition.setId(id);
                    beanDifinition.setClassName(className);
                    //获取bean标签中所有property标签
                    List<Element> propertyElements = element.elements("property");
                    ArrayList<PropertyDifinition> propertyDifinitions = new ArrayList<>();
                    //遍历所有property标签,将property标签信息存入PropertyDifinition
                    propertyElements.forEach(element1 -> {
                        PropertyDifinition propertyDifinition = new PropertyDifinition();
                        String name = element1.attributeValue("name");
                        String ref = element1.attributeValue("ref");
                        propertyDifinition.setName(name);
                        propertyDifinition.setRef(ref);
                        propertyDifinitions.add(propertyDifinition);
                    });
                    beanDifinition.setPropertyDifinitions(propertyDifinitions);
                    beanDifinitions.add(beanDifinition);
                });
            } catch (DocumentException e) {
                e.printStackTrace();
            }
        }
    }
​
    /**
     * 根据beanDifinitions内容反射构建类实例
     */
    public void initBean(){
        //遍历所有BeanDifinition,生成对应的Bean实例,存入IoC容器
        beanDifinitions.forEach(beanDifinition -> {
            try {
                String id = beanDifinition.getId();
                String className = beanDifinition.getClassName();
                //反射:根据类的全限定名字符串创建类的实例
                Class<?> c = Class.forName(className);
                Object instance = c.newInstance();
                //存入容器
                applicationContext.addBean(id,instance);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            }
        });
    }
​
    /**
     * 实现依赖注入
     */
    public void dependencyInjection(){
        beanDifinitions.forEach(beanDifinition -> {
            String id = beanDifinition.getId();
            String className = beanDifinition.getClassName();
            List<PropertyDifinition> propertyDifinitions = beanDifinition.getPropertyDifinitions();
            propertyDifinitions.forEach(propertyDifinition -> {
                String name = propertyDifinition.getName();
                String ref = propertyDifinition.getRef();
                //需要注入的实例
                Object originBean = applicationContext.getBean(id);
                //用于注入的实例
                Object refBean = applicationContext.getBean(ref);
                try {
                    //反射获取需要注入属性
                    Class<?> c = Class.forName(className);
                    Field field = c.getDeclaredField(name);
                    //强制放开私有属性赋值操作
                    field.setAccessible(true);
                    //给私有属性赋值
                    field.set(originBean,refBean);
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                } catch (NoSuchFieldException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            });
        });
    }
}
# 4.在Servlet类中测试
//测试之前应将UserService类中创建UserDao实例的代码去掉
public class UserService {
    private UserDao userDao;
    public void service(){
        System.out.println("service层方法执行!!!");
        userDao.dao();
    }
}
​
//使用自定义IoC框架中的容器获取UserService实例
public class UserServlet {
    public static void main(String[] args) {
//        UserService service = new UserService();
        ApplicationContext applicationContext = new ClasspathXmlApplicationContext();
        UserService userService = (UserService) applicationContext.getBean("userService");
        userService.service();
    }
}
# 5.增加注解配置
	修改xml配置文件,在配置文件中指定框架递归扫描哪些包下的注解
<?xml version="1.0" encoding="UTF-8" ?>
<beans>
    <!--指定框架启动后,扫描当前项目中所有com.woniu包下所有类上的注解-->
     <component-scan package="com.woniu"/>
</beans>
# 6.修改BeanFactory,增加扫描注解的代码
//创建注解
@Target(ElementType.TYPE)//作用于类上,为创建实例做准备
@Retention(RetentionPolicy.RUNTIME)
public @interface Component {
    String value() default "";
}

@Target(ElementType.FIELD)//作用于属性上,为依赖注入做准备
@Retention(RetentionPolicy.RUNTIME)
public @interface Resource {
}

//在UserDao、UserService类上加@Component注解,在UserService类中的userDao属性上增加@Resource注解

//增加容器类,用于注解配置
public class AnnotationConfigurationApplicationContext implements ApplicationContext{
    private Map<String,Object> beans=new HashMap<>();

    public AnnotationConfigurationApplicationContext() {
        BeanFactory beanFactory = new BeanFactory(this);
        beanFactory.initBean();
        beanFactory.dependencyInjection();
    }
    public void addBean(String id,Object object){
        beans.put(id,object);
    }

    public Object getBean(String id){
        return beans.get(id);
    }
}

//修改BeanFactory
public class BeanFactory {
    private List<BeanDifinition> beanDifinitions=new ArrayList<>();
    private ApplicationContext applicationContext;

    /**
     * 在构造方法中读取配置文件,将配置文件内容存入beanDifinitions
     */
    public BeanFactory(ApplicationContext applicationContext) {
        this.applicationContext=applicationContext;
        String path = this.getClass().getResource("/").getPath();//获取项目根目录
        path=path.substring(1).replace("/",File.separator);
        //如果是纯XML配置
        if (applicationContext instanceof ClasspathXmlApplicationContext) {
            ......
        }else if(applicationContext instanceof AnnotationConfigurationApplicationContext){//如果是注解配置
            try {
                //解析配置文件,获取扫描范围
                SAXReader saxReader = new SAXReader();
                Document document = saxReader.read(path+"applicationContext.xml");
                Element root = document.getRootElement();
                Element element = root.element("component-scan");
                String packageName = element.attributeValue("package");
                packageName=packageName.replace(".",File.separator);
                //递归扫描path所在路径下的所有文件
                File directory = new File(path+packageName);
                loopDirectory(directory,path);
            } catch (DocumentException e) {
                e.printStackTrace();
            }
        }
    }
    /**
     * 递归遍历包下所有类,将被注解的类生成实例
     */
    
    public void loopDirectory(File file,String path){
        if (file.isDirectory()) {//如果是目录,递归遍历
            File[] files = file.listFiles();
            if (files != null && files.length > 0) {
                for (File child : files) {
                    loopDirectory(child,path);
                }
            }
        }else{//如果是文件,获取文件的绝对路径,从中筛选出所有以.class结尾的文件
            String absolutePath = file.getAbsolutePath();
            if (absolutePath.endsWith(".class")) {
                //从绝对路径中获取类的全限定名
                //示例:绝对路径为E:\a\b\c\com\woniu\dao\UserDao.class,path为E:\a\b\c\
                //链式操作,replace(path,"")====》  com\woniu\dao\UserDao.class
                //replace(".class","")====》  com\woniu\dao\UserDao
                //replace(File.separator,".")====>  com.woniu.dao.UserDao
                String className=absolutePath.replace(path,"").replace(".class","").replace(File.separator,".");
                try {
                    Class<?> c = Class.forName(className);
                    //查看类上是否有@Component注解
                    if (c.isAnnotationPresent(Component.class)) {
                        BeanDifinition beanDifinition = new BeanDifinition();
                        beanDifinition.setClassName(className);
                        String id=null;
                        //如果@Component注解没有指定value,使用的默认值(即类名首字母小写)
                        if (!"".equals(c.getDeclaredAnnotation(Component.class))) {                            id=c.getSimpleName().substring(0,1).toLowerCase()+c.getSimpleName().substring(1);
                        }else{//如果指定了value,以指定值为准
                            id=c.getDeclaredAnnotation(Component.class).value();
                        }
                        beanDifinition.setId(id);
                        //获取类中所有属性
                        Field[] fields = c.getDeclaredFields();
                        ArrayList<PropertyDifinition> propertyDifinitions = new ArrayList<>();
                        //如果属性上有@Resource注解,则进行依赖注入
                        if (fields != null && fields.length > 0) {
                            for (Field field : fields) {
                                if (field.isAnnotationPresent(Resource.class)) {
                                    PropertyDifinition propertyDifinition = new PropertyDifinition();
                                    propertyDifinition.setName(field.getName());
                                    propertyDifinition.setRef(field.getName());
                                    propertyDifinitions.add(propertyDifinition);
                                }
                            }
                        }
                        beanDifinition.setPropertyDifinitions(propertyDifinitions);
                        beanDifinitions.add(beanDifinition);
                    }
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                }

            }
        }
    }
	......
}
# 7.测试
public class UserServlet {
    public static void main(String[] args) {
//        UserService service = new UserService();
        AnnotationConfigurationApplicationContext applicationContext = new AnnotationConfigurationApplicationContext();
        UserService userService = (UserService) applicationContext.getBean("userService");
        userService.service();
    }
}

spring:XML实现

入门案例

需求:搭建spring IoC环境,使用spring容器创建并管理类的对象。

步骤1:创建web项目,将spring容器中的4个jar包:spring-core、spring-context、spring-expression、spring-beans以及spring-jcl导入在WEB-INF/lib目录中。如果希望使用log4j,还应导入对应的jar包。

注:spring的日志功能依赖于commons-logging,spring-jcl包中集成了commons-logging,较老版本的sping中没有spring-jcl,此时必须单独导入commons-logging。

步骤2:创建实体类src/com/wsjy/domain/Subject.java

public class Subject implements Serializable {

}

步骤3:创建spring核心配置文件src/applicationContext.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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd">
	<!-- 
		bean标签对应于实体类对象,在java程序中可以通过容器的getBean(id属性值)获取实体类对象
		id属性是实体类对象的唯一标识
		class属性是通过反射创建对象时的全限定类名
	-->
    <bean id="subject" class="com.wsjy.domain.Subject"/>

</beans>

注:标签中的属性部分无需手动配置,可在spring解压目录:spring-framework-5.2.2.RELEASE\docs\spring-framework-reference\core.html中Ctrl+F查找xmlns,即可找到相应内容复制即可。

步骤4:创建测试类src/com/wsjy/test/Test.java

public class Test {
    public static void main(String[] args) {
        //1、读取配置文件,生成spring容器
        ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");

        //2、根据bean标签id获取spring容器管理的bean实例
        Subject sub = (Subject) ac.getBean("subject");
        System.out.println(sub);
    }
}

运行测试类后,可在控制台看到对应Subject实例信息被打印,在测试类中并没有手动创建Subject类实例,而是通过spring容器获取的Subject类对象,由此可知,是spring容器创建并管理了Subject类对象。

spring创建对象(控制反转)的三种方式

使用构造方法创建

入门案例中使用的就是构造方法创建,使用的是默认的无参构造。

使用工厂创建

在实际应用开发过程中,会使用到很多的第三方类库,第三方类库中的类都是class字节码文件,无法通过修改源码的形式提供构造方法,此时类的对象一般情况下是通过工厂模式来提供的,spring核心容器也提供了根据工厂模式来创建和管理类的方法。

使用实例工厂创建

需求:模拟实例工厂创建。

步骤1:

创建Subject子类:src/com/wsjy/domain/PhysicsSubject.java

public class PhysicsSubject extends Subject implements Serializable {

}

创建Subject子类:src/com/wsjy/domain/LiteratureSubject.java

public class LiteratureSubject extends Subject implements Serializable {

}

步骤2:创建工厂类src/com/wsjy/factory/SubjectFactory.java

public class SubjectFactory {
	//工厂方法,根据sub_no生产不同的Subject子类
    public Subject getSubject(Integer sub_no){
        switch (sub_no) {
            case 1:
                return new PhysicsSubject();
            case 2:
                return new LiteratureSubject();
            default:
                return null;
        }
    }
}

步骤2:修改核心配置文件src/applicationContext.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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd">
	<!-- 创建工厂实例 -->
    <bean id="subjectFactory" class="com.wsjy.factory.SubjectFactory"/>

	<!-- 
		使用工厂创建Subject类的实例
		factory-bean属性指定创建当前类的实例使用哪个工厂
		factory-method属性指定创建当前类的实例使用的是工厂的哪个方法
 	-->
    <bean id="subject" factory-bean="subjectFactory" factory-method="getSubject">
        <!-- 使用constructor-arg为工厂方法传递参数 -->
        <constructor-arg name="sub_no" value="2"/>
    </bean>
</beans>
使用静态工厂创建

需求:模拟静态工厂创建。

步骤1:修改工厂类src/com/wsjy/factory/SubjectFactory.java

public class SubjectFactory {
	//将原本的工厂方法修改成静态方法
    public static Subject getSubject(Integer sub_no){
        switch (sub_no) {
            case 1:
                return new PhysicsSubject();
            case 2:
                return new LiteratureSubject();
            default:
                return null;
        }
    }
}

步骤2:修改核心配置文件src/applicationContext.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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd">
    <!-- 
		创建Subject类实例 
		class属性指定当前实例由哪个类来创建
		factory-method属性指定创建类实例的方法
	-->
	<bean id="subject" class="com.wsjy.factory.SubjectFactory" 
          factory-method="getSubject">
        <!-- 使用constructor-arg为工厂方法传递参数 -->
        <constructor-arg name="sub_no" value="2"/>
    </bean>
</beans>

spring依赖注入的三种方式

spring依赖注入有以下三种方式:

  • 构造器注入:依赖于类的构造方法实现,构造方法可有参也可无参;
  • setter注入:依赖于类的setter方法实现,灵活且可读性高,这是spring中最主流的注入方式;
  • 接口注入:当注入的资源并非来自本系统,而是来自于系统外部,比如数据库连接资源在Tomcat下配置,并通过JNDI的方式去获取,此时数据库资源就属于外部资源,可以使用接口注入方式获取它;

接口注入模式因为具备侵入性,它要求组件必须与特定的接口相关联,因此并不被看好,实际使用有限。

构造器注入

需求:使用带参构造为spring管理的Subject实例注入相应属性值。

步骤1:修改src/com/wsjy/domain/Subject.java

public class Subject implements Serializable {
    private Integer subNo;
    private String subName;

    public Subject(Integer subNo, String subName) {
        this.subNo = subNo;
        this.subName = subName;
    }
	
    /*toString*/
}

步骤2:修改核心配置文件src/applicationContext.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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="subject" class="com.wsjy.domain.Subject">
        <constructor-arg name="subNo" value="1"/>
        <constructor-arg name="subName" value="HTML"/>
    </bean>

</beans>

标签属性详解

  • type:为构造方法中的某个类型的参数赋值;
  • index:指定为构造方法中某个索引的参数赋值,索引从0开始计数;
  • name:指定为构造方法中某个名称为name属性取值的参数赋值,一般使用该方式;
  • value:被赋值的数据。
  • ref:指定为构造方法中某个参数赋值,该值的类型不是java基本数据类型和String类型,而是一个在spring容器中已被注册的bean;
setter注入

需求:使用setter方法为spring管理的Subject实例注入相应属性值。

步骤1:修改src/com/wsjy/domain/Subject.java

package com.wsjy.domain;

import java.io.Serializable;

public class Subject implements Serializable {
    private Integer subNo;
    private String subName;

    public Integer getSubNo() {
        return subNo;
    }

    public void setSubNo(Integer subNo) {
        this.subNo = subNo;
    }

    public String getSubName() {
        return subName;
    }

    public void setSubName(String subName) {
        this.subName = subName;
    }

    /*toString*/
}

步骤2:修改核心配置文件src/applicationContext.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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd">

        <bean id="subject" class="com.wsjy.domain.Subject">
            <property name="subNo" value="1"/>
            <property name="subName" value="html"/>
        </bean>

</beans>

与类似,但没有type和index属性。

接口注入

接口注入的形式很少见,一般用于注入外部资源。

需求:Tomcat中的web项目使用了spring,通过spring的机制,使用JNDI获取Tomcat启动的数据库连接池。

步骤1:在web/META-INF目录中新建context.xml,如果没有META-INF,需要手动添加;

<?xml version="1.0" encoding="UTF-8" ?>
<Context>
    <Resource name="mydb" auth="Container" type="javax.sql.DataSource"
   maxActive="100" maxIdle="30" maxWait="10000"
   username="root" password="root" driverClassName="com.mysql.jdbc.Driver"
   url="jdbc:mysql://localhost:3306/taotao?useUnicode=true&amp;characterEncoding=utf-8"/>
</Context>

步骤2:修改核心配置文件src/applicationContext.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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd">
    
	<bean id="dataSource"
          class="org.springframework.jndi.JndiObjectFactoryBean">
    	<property name="jndiName">
        	<value>java:comp/env/mydb</value>
        </property> 
    </bean>

</beans>

步骤3:修改index.jsp

<%@ page import="javax.naming.NamingException" %>
<%@ page import="javax.sql.DataSource" %>
<%@ page import="java.sql.SQLException" %>
<%@ page import="java.sql.Connection" %>
<%@ page import="java.sql.PreparedStatement" %>
<%@ page import="java.sql.ResultSet" %>
<%@ page import="org.springframework.context.support.ClassPathXmlApplicationContext" %>
<%@ page import="org.springframework.context.ApplicationContext" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
  <head>
    <title>$Title$</title>
  </head>
  <body>
  <%
      //获取spring容器
      ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
      //通过spring容器获取注册的数据源对象
      DataSource ds = ac.getBean("dataSource", DataSource.class);
      try {
        Connection conn = ds.getConnection();
        PreparedStatement ps = conn.prepareStatement("select * from t_subject");
        ResultSet rs = ps.executeQuery();
        while (rs.next()) {
          out.print(rs.getInt("sub_no")+"\t");
          out.print(rs.getString("sub_name")+"\t");
        }
        rs.close();
        ps.close();
        conn.close();
      } catch (SQLException e) {
        e.printStackTrace();
      }
  %>
  </body>
</html>

启动tomcat,可看到数据库t_subject表中内容在浏览器中全部展示。

依赖注入的三种类型
基本数据类型、String

基本数据类型和String可以直接在value处赋值,spring容器会自动完成类型转换并将值注入到对象属性中。

在容器中注册过的bean

在或中使用ref属性引用在spring容器中注册过的bean。

需求:为Student类的birthday属性注入日期类型。

步骤1:创建实体类src/com/wsjy/domain/Student.java

public class Student implements Serializable {
    private Integer sid;
    private String sname;
    private Integer ssex;
    private Integer sage;
    private String saddress;
    private java.sql.Date sbirthday;
    private Integer cid;

	/*setter/getter/toString*/
}

注:为了与数据库表字段属性进行对应,此处的birthday属性类型是java.sql.Date,而不是java.util.Date。

步骤2:修改核心配置文件src/applicationContext.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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd">

	<bean id="student" class="com.wsjy.domain.Student">
        <property name="sid" value="1"/>
        <property name="sname" value="张三"/>
        <property name="ssex" value="1"/>
        <property name="sage" value="35"/>
        <property name="saddress" value="北京海淀"/>
        <property name="sbirthday" ref="sqlDate"/>
        <property name="cid" value="3"/>
    </bean>

    <bean id="sqlDate" class="java.sql.Date">
        <constructor-arg name="year" value="95"/>
        <constructor-arg name="month" value="5"/>
        <constructor-arg name="day" value="25"/>
    </bean>

</beans>

注:可选择设置标签的autowire属性设置容器中注入过的bean的自动注入,也可在标签上设置default-autowire设置全局自动注入。

autowire属性常用取值:

  • byName,如果容器中有注册的bean名称与实体类属性名称相同,自动注入
  • byType,如果容器中有注册的bean的类型与实体类属性类型相同,自动注入
  • constructor,使用构造方法注入,底层还是byName
  • no,不自动注入
  • default,使用全局自动注入的值,如果没有设置全局自动注入,默认no

一般来说,不会配置全局自动注入,另外自动注入一般也不会在XML文件中配置,而是通过注解来实现,因为注解的自动注入功能比XML配置要更强大。

复杂类型(集合类型)

spring容器可以注入的复杂类型有数组、List、Set、Map、Properties类型。

需求:一个学生学习多个科目,分别使用数组、List、Set、Map、Properties进行实现。

步骤1:修改实体类src/com/wsjy/domain/Student.java

public class Student implements Serializable {
    private Integer sid;
    private String sname;
    private Integer ssex;
    private Integer sage;
    private String saddress;
    private Date sbirthday;
    private Integer cid;

    //private Subject[] subs;
	//private List<Subject> subs;
    //private Set<Subject> subs;
    //private Map<Integer,Subject> subs;
    private Properties subs;
      
	/*setter/getter/toString*/
}

步骤2:修改核心配置文件src/applicationContext.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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="student" class="com.wsjy.domain.Student">
        <property name="sid" value="1"/>
        <property name="sname" value="张三"/>
        <property name="ssex" value="1"/>
        <property name="sage" value="35"/>
        <property name="saddress" value="北京海淀"/>
        <property name="sbirthday" ref="sqlDate"/>
        <property name="cid" value="3"/>
        <property name="subs">
            <!--
 				数组:<array>
				List:<list>
				Set:<set>
				测试时更改标签即可。
            <array>
                <bean class="com.wsjy.domain.Subject">
                    <property name="subNo" value="1"/>
                    <property name="subName" value="html"/>
                </bean>
                <bean class="com.wsjy.domain.Subject">
                    <property name="subNo" value="2"/>
                    <property name="subName" value="java"/>
                </bean>
                <bean class="com.wsjy.domain.Subject">
                    <property name="subNo" value="3"/>
                    <property name="subName" value="python"/>
                </bean>
            </array>-->
            
            <!--
 				Map:<map>和<entry>
				Properties:<pros>和<prop>
				应注意的是,properties不能保存对象,只能保存值。
			
            <map>
                <entry key="1">
                    <bean class="com.wsjy.domain.Subject">
                        <property name="subNo" value="1"/>
                        <property name="subName" value="html"/>
                    </bean>
                </entry>
                <entry key="2">
                    <bean class="com.wsjy.domain.Subject">
                        <property name="subNo" value="2"/>
                        <property name="subName" value="java"/>
                    </bean>
                </entry>
                <entry key="3">
                    <bean class="com.wsjy.domain.Subject">
                        <property name="subNo" value="3"/>
                        <property name="subName" value="python"/>
                    </bean>
                </entry>
            </map>-->
			<!--properties的用法-->
			<props>
                <prop key="1">html</prop>
				<prop key="2">java</prop>
				<prop key="3">python</prop>
    		</props>
			
        </property>
    </bean>


    <bean id="sqlDate" class="java.sql.Date">
        <constructor-arg name="year" value="95"/>
        <constructor-arg name="month" value="5"/>
        <constructor-arg name="day" value="25"/>
    </bean>

</beans>

注:

  1. list、set、array可以互换;
  2. 在集合中保存的不是对象时,map和props可以互换;
spring p命名空间和c命名空间

在通过构造方法或set方法给bean注入关联项时通常是通过constructor-arg元素和property元素来定义的。在有了p命名空间和c命名空间时我们可以简单的把它们当做bean的一个属性来进行定义。

p命名空间

使用p命名空间时需要先声明使用对应的命名空间,即在beans元素上加入xmlns:p="www.springframework.org/schema/p"。

c命名空间

c命名空间的用法和p命名空间类似,其对应于constructor-arg,即可以将constructor-arg元素替换为bean的一个以c命名空间前缀开始的属性。使用c命名空间之前也需要通过xmlns:c=”www.springframework.org/schema/c”进行声明。

P命名空间和c命名空间使用案例:

<?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:p="http://www.springframework.org/schema/p"
       xmlns:c="http://www.springframework.org/schema/c"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd">
    <!-- 使用property子标签注入 -->
	<!--<bean id="student" class="com.wsjy.domain.Student">
        <property name="sid" value="1"/>
        <property name="sname" value="张三"/>
        <property name="ssex" value="1"/>
        <property name="sage" value="35"/>
        <property name="saddress" value="北京海淀"/>
        <property name="sbirthday" ref="sqlDate"/>
        <property name="cid" value="3"/>
    </bean>-->
	
    <!-- 使用p命名空间通过属性形式进入注入,属性为引用类型时,在属性名后加-ref引用即可 -->
    <bean id="student" class="domain.Student" p:sid="1" p:sname="张三" p:ssex="1"
           p:sage="35" p:saddress="北京海淀" p:sbirthday-ref="sqlDate" p:cid="3"/>
	
    <!-- 使用constructor-arg子标签注入 -->
 	<!--<bean id="sqlDate" class="java.sql.Date">
        <constructor-arg name="year" value="95"/>
        <constructor-arg name="month" value="5"/>
        <constructor-arg name="day" value="25"/>
    </bean>-->
	
    <!-- 使用c命名空间通过属性形式进入注入,属性为引用类型时,在属性名后加-ref引用即可
 		第一种方式按构造方法参数名称注入(按日期对象构造方法参数名称注入有bug,构造的日期对象不正确)
		第二种方式按构造方法参数位置注入
	-->
    <!--<bean id="sqlDate" class="java.sql.Date" c:year="95" c:month="5" c:day="25"/>-->
   	<bean id="sqlDate" class="java.sql.Date" c:_0="95" c:_1="5" c:_2="25"/>

</beans>

获取spring容器的三种方式

在入门案例的测试类中,先获取spring容器ApplicationContext后,再调用其方法获取其创建和管理的对象。ApplicationContext是BeanFactory接口的子接口之一,它对BeanFactory的功能做了很多有用的扩展,绝大部分情况下会使用ApplicationContext作为IoC容器。

BeanFactory和ApplicationContext的区别:

  • BeanFactory创建对象是延迟加载的,即什么时候调用对象,什么时候创建;
  • ApplicationContext创建对象则更智能,会根据对象的单例还是多例,来选择是否延迟加载(单例立即加载,多例延迟加载);

创建ApplicationContext容器的三种方式

  • 解析类路径下的XML文件创建(ClassPathXmlApplicationContext),要求配置文件必须存在于类路径下;
  • 解析系统文件路径下的XML文件创建(FileSystemXmlApplicationContext),配置文件可以在系统的任意路径中;
  • 解析注解创建(AnnotationConfigApplicationContext);

一般来说,不建议使用FileSystemXmlApplicationContext,因为系统路径的权限是否开放决定了是否能够访问到该路径下的文件。当权限没有开放时,由于访问不到该路径,因此加载不到配置文件,会出现异常。

bean的作用域

spring容器通过标签的scope属性控制其作用域。

<bean id="subject" class="com.wsjy.domain.Subject" scope="singleton">
 <property name="subNo" value="2"/>
 <property name="subName" value="html"/>
</bean>

在 Spring IoC 容器中具有以下几种作用域:

  • singleton:单例模式,默认,在整个Spring IoC容器中,使用singleton定义的Bean将只有一个实例,适用于无状态bean;
  • prototype:原型模式(多例),每次通过容器的getBean方法获取prototype定义的Bean时,都将产生一个新的Bean实例,适用于有状态的Bean;
  • request:对于每次HTTP请求,使用request定义的Bean都将产生一个新实例,即每次HTTP请求将会产生不同的Bean实例。在Web应用中使用Spring时,该作用域才有效;
  • session:对于每次HTTP Session,使用session定义的Bean都将产生一个新实例。在Web应用中使用Spring时,该作用域才有效;
  • globalsession:每个全局的HTTP Session,使用session定义的Bean都将产生一个新实例。典型情况下,仅在使用portlet context的时候有效。在集群环境下使用spring时,该作用域生效,如不是集群环境,该作用域等同于session。

bean的生命周期

根据bean的作用域不同,其生命周期也是不相同的。

单例对象

  • 出生:容器创建时,由于单例对象会立即加载,因此单例也就随着容器的创建就被创建了;
  • 存活:当容器一直存在时,单例对象也一起存在;
  • 死亡:当容器销毁时,单例对象随着容器的销毁一起销毁。

也就是说,单例对象的生命周期与spring容器保持一致。

多例对象

  • 出生:容器创建时,多例对象延迟加载,直到使用该对象时spring容器执行创建操作;
  • 存活:只要对象被使用,对象就一直存在;
  • 死亡:多例对象不会随着容器销毁而销毁,它的销毁由java垃圾回收机制决定;

需求:测试bean的生命周期与作用域之间的关系。

步骤1:修改src/com/wsjy/domain/Subject.java

public class Subject implements Serializable {
	......
        
    public void init(){
        System.out.println("subject对象被创建了。。。");
    }

    public void destroy(){
        System.out.println("subject对象被销毁了。。。");
    }
    
    ......
}

步骤2:修改核心配置文件src/applicationContext.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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="subject" class="com.wsjy.domain.Subject" 
          scope="prototype" init-method="init" destroy-method="destroy">
        <property name="subNo" value="1"/>
        <property name="subName" value="html"/>
    </bean>

</beans>

步骤3:修改测试类

public class Test {
    public static void main(String[] args) {
        //1、读取配置文件,生成spring核心容器
        //注意,此处不使用多态,因为ApplicationContext接口中没有close()方法
        ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");

        //2、根据bean标签id获取spring核心容器管理的bean实例
        Subject sub = (Subject) ac.getBean("subject");
        System.out.println(sub);
        //3、关闭spring容器
        ac.close();
    }
}

运行测试类可以发现:当标签scope属性为singleton时,spring容器关闭时,subject对象被销毁;当标签scope属性为prototype时,spring容器关闭时,subject对象没有被销毁。

spring:注解实现

在实际应用开发过程中,更多的会考虑使用注解而不是XML来装配bean。因为使用注解的方式可以大大减少XML配置,且功能更为强大。注解不但实现了XML的功能,还提供了自动装配功能,采用了自动装配后,开发人员需要做的决断就变少了,从而更有利于程序的开发,这也体现了“约定优于配置”的开发原则。

在spring中,提供了两种方式来让spring容器发现bean:

  • 组件扫描:通过定义资源的方式,让spring容器扫描对应的包,从而把bean装配进来;
  • 自动装配:通过注解定义,使得一些依赖关系可以通过注解完成;

通过扫描和自动装配,绝大部分的项目都可以用java配置完成,而不是XML,目前注解已经成为spring开发的主流。

spring IoC容器使用的注解有四类:

  • 创建对象,其作用相当在配置一个标签;
  • 注入数据,其作用相当于在标签中配置子标签;
  • 控制作用域,其作用相当于为标签配置scope属性;
  • 生命周期相关,其作用相当于为标签配置init-method和destroy-method属性;

创建对象

spring提供了4个注解用于创建对象:@Component、@Controller、@Service、@Repository。这4个注解的作用都是一样的,都用于将被注解的类存入spring容器,让spring容器管理该类的对象。

@Controller、@Service、@Repository的语义更强,它们是spring用于区分三层架构而设计的。一般来说,@Controller用于表现层(控制层),@Service用于业务层,@Repository用于持久层。

需求:使用注解将src/com/wsjy/pojo/Subject.java注册到spring容器中。

步骤1:将spring-aop包导入项目,要使用注解,需要aop的支持。

步骤2:修改核心配置文件src/applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>
<!--导入context命名空间及约束-->
<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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">
	
    <!-- 使用context扫描对应包下的所有类,并将其存入到spring容器 -->
    <context:component-scan base-package="com.wsjy"/>

</beans>

步骤3:修改src/com/wsjy/domain/Subject.java

/**
 * @Component只有一个value属性,用于设置被注解的类在spring容器中的id值
 * 如省略不写,默认以被注解类首字母小写的类名作为id
 * @Component("subject")或@Component(value="subject")都是合法的
 */
@Component
public class Subject implements Serializable {
	......
}

运行测试类,控制台可打印subject对象。

以上案例也可以使用纯注解实现,使用自定义的配置类来代替心配置文件:

创建一个无逻辑的配置类来代替核心配置文件src/com/wsjy/domain/ApplicationConfig.java

/**
 * @Configuration注解的类会被spring认为是一个配置类,相当于核心配置文件
 * @ComponentScan代表进行扫描,有两个配置项:basePackages\basePackageClasses
 * 当不写任何配置项时,默认扫描被注解的ApplicationConfig所在包中的所有被@Component注解的类
 * 当配置basePackages时,basePackages属性取值为一个包的完全限定名数组,
 * 表示spring容器会去把数组中存在的包下所有被@Component注解的类装配成bean
 * 当配置basePackageClasses时,basePackageClasses属性取值为一个类或者接口的完全限定名数组,
 * 表示spring容器会去把数组中存在的类或者接口的实现类装配成bean
 * basePackages和basePackageClasses可以同时存在,spring会进行区分,不会重复配置生成多个对象
 * basePackages等同于value,查看源码可知,是通过别名来实现的
*/
@Configuration
@ComponentScan(value = {"com.wsjy"})
//@ComponentScan(basePackages = {"com.wsjy"},basePackageClasses = {SubjectFactory.class})
public class ApplicationConfig {
}

修改测试类

public class Test {
	public static void main(String[] args) {
//1、通过反射生成spring核心容器
	ApplicationContext ac = new AnnotationConfigApplicationContext(ApplicationConfig.class);
  //2、根据bean标签id获取spring核心容器管理的bean实例
 	Subject sub = (Subject) ac.getBean("subject");
 	System.out.println(sub);
	}
}

运行测试类,与使用核心配置文件得到的结果是相同的。

注入数据

基本类型和String

spring注入数据时,如果是基本数据类型或String时,使用@Value注解来实现。

需求:上例中的subject对象可以创建,但对象属性为null,使用@Value注解为属性赋值。

修改src/com/wsjy/domain/Subject.java

@Component
public class Subject implements Serializable {
    //@value只有一个value属性,可省略不写
    @Value("1")
    private Integer subNo;
    @Value("html")
    private String subName;
}

注:使用@Value注解为成员变量赋值,是通过反射实现的,无需提供setter方法。

在容器中注册过的bean

spring注入在容器中注册过的bean时,可以使用自动装配@Autowired或@Resource,该注解可以在spring容器中自动寻找与被注解成员变量类型相同的bean装配到该变量。

@Autowired:默认按类型注入,相同类型的bean有多个时,需要进行处理,否则报异常。

@Resource:默认按名称注入,如果名称对应的bean与属性类型不匹配,报异常,如果没有找到相应名称,则按类型注入,需要进行处理,否则报异常。

需求:为Student类的Classes(班级)属性注入Classes对象。

步骤1:创建班级类Classes.java

@Component
public class Classes {
    @Value("1")
    private Integer cid;
    @Value("java4")
    private String cname;
    @Value("28")
    private Integer studentNumber;
    //使用SpEL(spring的EL表达式)设置java.sql.Date类型的值
    @Value("#{T(java.sql.Date).valueOf('2019-8-4')}")
    private Date openingDate;

    @Override
    public String toString() {
        return "Classes{" +
                "cid=" + cid +
                ", cname='" + cname + ''' +
                ", studentNumber=" + studentNumber +
                ", openingDate=" + openingDate +
                '}';
    }
}

步骤2:修改Student.java

@Component
public class Student implements Serializable {
    @Value("3")
    private Integer sid;
    @Value("tom")
    private String sname;
    @Value("1")
    private Integer ssex;
    @Value("23")
    private Integer sage;
    @Value("重庆渝中")
    private String saddress;
    @Value("#{T(java.sql.Date).valueOf('1997-1-1')}")
    private Date sbirthday;
    @Autowired
    private Classes cls;

    @Override
    public String toString() {
        return "Student{" +
                "sid=" + sid +
                ", sname='" + sname + ''' +
                ", ssex=" + ssex +
                ", sage=" + sage +
                ", saddress='" + saddress + ''' +
                ", sbirthday=" + sbirthday +
                ", cls=" + cls +
                '}';
    }
}

运行测试类代码,可以看到Student类的Classes类型属性cls已被自动装配了。

使用自动装配@Autowired是有歧义的。按照spring的建议,大部分情况下会使用面向接口编程,但一个接口可能会有多个实现类。多个实现类在注册到spring容器时,就出现了类型相同,而id值不相同的情况,此时通过自动装配为成员变量赋值,会出现多个类型相同的bean,此时spring不知如何选择,会报异常。

同理,抽象类的子类也可能有多个,此时自动装配时如果使用了父类类型,会出现相同的问题。

需求:使用Classes类的子类JavaClasses或UiClasses实现Student类cls属性的自动装配。

步骤1:修改班级类Classes.java

public abstract class Classes {
    public Integer cid;
    public String cname;
    public Integer studentNumber;
    public Date openingDate;

    @Override
    public String toString() {
        return "Classes{" +
                "cid=" + cid +
                ", cname='" + cname + ''' +
                ", studentNumber=" + studentNumber +
                ", openingDate=" + openingDate +
                '}';
    }
}

步骤2:创建Classes类的子类

JavaClasses.java

@Component
public class JavaClasses extends Classes {
    @Value("1")
    public Integer cid;
    @Value("java4")
    public String cname;
    @Value("28")
    public Integer studentNumber;
    @Value("#{T(java.sql.Date).valueOf('2019-8-4')}")
    public Date openingDate;
    
    /*toString*/
}

UiClasses.java

@Component
public class UiClasses extends Classes {
    @Value("2")
    public Integer cid;
    @Value("ui3")
    public String cname;
    @Value("32")
    public Integer studentNumber;
    @Value("#{T(java.sql.Date).valueOf('2019-8-10')}")
    public Date openingDate;
    
    /*toString*/
}

此时直接运行测试类,会报异常No qualifying bean of type 'com.wsjy.domain.Classes' available: expected single matching bean but found 2: javaClasses,uiClasses,其含义是预期只会找到1个匹配,但实际找到了2个匹配,spring不知道如何处理,抛出异常。

要解决自动装配@Autowired产生的歧义,有以下两种方式:@Primary,@Qualifier。

@Primary:该注解代表首要的,它会告诉spring,如果spring容器中相同类型的bean出现了多个时,优先使用用哪个bean进入注入。

修改JavaClasses.java

@Component
//标注优先注入的bean
@Primary
public class JavaClasses extends Classes {
	......
}

运行测试类,会发现JavaClasses被注入。

@Qualifier:该注解会在出现类型相同的bean时,告诉spring,应该注入由@Qualifier指定名称的bean;

修改Student.java

@Component
public class Student implements Serializable {
    ......
    @Autowired
    //@Qualifier的value属性指定的是bean在spring容器中注册的id值
    @Qualifier("uiClasses")
    private Classes cls;
	......
}

运行测试类,会发现UiClasses被注入。

注:同时出现@Primary,@Qualifier时,以@Qualifier指定的为准。而且实际开发过程中,一般不会使用@Primary,更多会使用@Qualifier。因为@Primary只能解决优先级问题,无法解决选择性的问题。

@Resource注解代替@Autowired和@Qualifier。

修改Student.java

@Component
public class Student implements Serializable {
    ......
    //@Resource的name属性指向bean在spring容器中注册的id值
    @Resource(name="javaClasses")
    private Classes cls;
	......
}

运行测试类,会发现JavaClasses被注入。

注:@Resource依赖于javax.annotation-api-1.2.jar,若项目中没有导入对应依赖,无法使用。

@Value、@Autowired、@Qualifier还可以用装配带参构造方法。

修改Student.java

@Component
public class Student implements Serializable {

    private Integer sid;
    private String sname;
    private Integer ssex;
    private Integer sage;
    private String saddress;
    private Date sbirthday;
    private Classes cls;

    public Student(@Value("10") Integer sid,@Value("张三丰") String sname,
                   @Value("1") Integer ssex, @Value("120") Integer sage,
                   @Value("武当山") String saddress, @Value("#{T(java.sql.Date).valueOf("1600-5-5")}") Date sbirthday,
                   @Qualifier("uiClasses") Classes cls) {
        this.sid = sid;
        this.sname = sname;
        this.ssex = ssex;
        this.sage = sage;
        this.saddress = saddress;
        this.sbirthday = sbirthday;
        this.cls = cls;
    }
	/*toString*/
}

运行测试类,Student类对象相关属性的值被输出。

注:装配构造方法参数时,@Qualifier无需与@Autowired配合即可使用(如果装配到成员变量,@Qualifier必须与@Autowired配合使用)。

如果只使用@Autowired,程序装配失败,因为有多个同类型的bean存在。如果没有使用@Primary指定会优先装配的bean时,应使用@Qualifier选择性的进行装配。

复杂类型(集合类型)

使用注解无法注入复杂类型,只能通过XML才能实现。

控制作用域

spring通过@Scope注解来控制bean的作用域,该注解的value属性用于指定作用域。

修改Student.java

@Component
@Scope("prototype")
public class Student implements Serializable {
   ......
}

生命周期相关

与bean的生命周期相关的两个注解:@PostConstruct,@PreDestroy。@PostConstruct对应于标签的init-method属性,@PreDestroy对应于标签的destroy-method属性。

注:@PostConstruct,@PreDestroy依赖于javax.annotation-api-1.2.jar,若项目中没有导入对应依赖,无法使用。

修改Subject.java

@Component
public class Subject implements Serializable {
    ......
    
    @PostConstruct
    public void init(){
        System.out.println("subject对象被创建了。。。");
    }
    @PreDestroy
    public void destroy(){
        System.out.println("subject对象被销毁了。。。");
    }
    
    ......
}

使用注解替换核心配置文件

spring提供了自定义配置类来替换spring核心配置文件的功能。

需求:使用注解实现自定义配置类替换spring核心配置文件。

spring核心配置文件

<?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
        https://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
        <property name="jndiName" value="java:comp/env/mydb"/>
    </bean>
</beans>

步骤1:在项目中导入commons-dbcp-1.4.jar和commons-pool-1.5.4.jar

步骤2:创建自定义配置类src/config/ApplicationConfig.java

@Configuration
@ComponentScan(value = {"com.wsjy"})
//@ComponentScan(basePackages = {"com.wsjy"},basePackageClasses = {SubjectFactory.class})
public class ApplicationConfig {
    //获取DBCP数据源,要使用该技术,需要在项目导入commons-dbcp和commons-pool依赖。
    @Bean(name="dataSource")
    public DataSource getDataSource(){
        Properties p=new Properties();
        p.setProperty("driver","com.mysql.jdbc.Driver");
        p.setProperty("url","jdbc:mysql://localhost:3306/taotao");
        p.setProperty("username","root");
        p.setProperty("password","root");
        DataSource ds=null;
        try {
            ds= BasicDataSourceFactory.createDataSource(p);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return ds;
    }
}

@Configuration注解的类会被spring认为是一个配置类,相当于创建了一个核心配置文件。

@ComponentScan表示进行扫描,有两个配置项:basePackages\basePackageClasses。

  • 当不写任何配置项时,默认扫描被注解的ApplicationConfig所在包中的所有被@Component注解的类;
  • 当配置basePackages时,basePackages属性取值为一个包的完全限定名数组,表示spring容器会把数组中存在的包下所有被@Component注解的类装配成bean;
  • 当配置basePackageClasses时,basePackageClasses属性取值为一个类或者接口的完全限定名数组,表示spring容器会把数组中存在的类或者接口的实现类装配成bean;

basePackages和basePackageClasses可以同时存在,spring会进行区分,不会重复配置生成多个对象。basePackages等同于value,查看源码可知,是通过别名来实现的。

@Bean注解用于将某个方法返回的对象注册到spring容器中,一般用于第三方类库的类(比如数据库连接池等)。该注解有两个等效的配置项:value和name,是一个String类型的数组,其值用于设定注册到spring容器的bean名称,如果不设,默认将当前方法名作为注册到spring容器中的bean的id。

步骤3:修改测试类

public class Test {
	public static void main(String[] args) {
     //1、通过反射生成spring核心容器
    	ApplicationContext ac = new AnnotationConfigApplicationContext(ApplicationConfig.class);
     //2、根据bean标签id获取spring核心容器管理的bean实例
    DataSource ds = ac.getBean("dataSource", DataSource.class);
        try {
            Connection conn = ds.getConnection();
            PreparedStatement ps = conn.prepareStatement("select * from t_subject");
            ResultSet rs = ps.executeQuery();
            while (rs.next()) {
                System.out.print(rs.getInt("sub_no")+"\t");
                System.out.println(rs.getString("sub_name"));
            }
            rs.close();
            ps.close();
            conn.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

使用注解实现的自定义配置类代替了核心配置文件,因此获取spring容器的方式也需进行变化,需要使用

AnnotationConfigApplicationContext对象来获取spring容器,其构造方法是一个可变长度的参数数组。

另外,@Configuration可以省略,如果不配,则必须将自定义配置类作为参数传递给AnnotationConfigApplicationContext的构造方法,否则spring不认为该类为配置类,就不会去扫描类中的注解。

项目中可以存在多个配置文件,比如在项目的主配置文件中存放公共配置,某些具体的配置(比如说获取数据库连接)则会单独创建一个配置文件。非主配置文件上一般都会使用@Configuration注解,因为在使用AnnotationConfigApplicationContext获取容器时使用参数过多,会造成代码阅读困难,且容易遗漏。

存在多个配置文件时,也可以使用@Import注解,将非主配置类导入主配置类。@Import注解只有一个value属性可以配置,是一个Class类型的数组,用于指定其他配置类的字节码。

需求:创建多个配置文件,使用@Import将对应配置导入主配置类。

步骤1:创建配置类src/config/JdbcConfig.java,将ApplicationConfig中获取数据源的代码转移到本类中。

@Configuration
public class JdbcConfig {
    @Bean(name="dataSource")
    public DataSource getDataSource(){
        Properties p=new Properties();
        p.setProperty("driver","com.mysql.jdbc.Driver");
        p.setProperty("url","jdbc:mysql://localhost:3306/taotao");
        p.setProperty("username","root");
        p.setProperty("password","root");
        DataSource ds=null;
        try {
            ds= BasicDataSourceFactory.createDataSource(p);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return ds;
    }
}

步骤2:修改配置类src/config/ApplicationConfig.java

@Configuration
@ComponentScan(value = {"com.wsjy","config"})
@Import(JdbcConfig.class)
public class ApplicationConfig {

}

运行测试类,可以正常运行。

上例中的JdbcConfig类中通过硬编码,将获取数据源的jdbc属性写在源码中,不合理。一般会使用软编码,即用属性文件保存jdbc连接所需要的值,可以使用@PropertySource注解读取属性文件获取对应的值。

需求:使用Properties文件保存jdbc连接属性。

步骤1:创建属性文件src/jdbc.properties

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/taotao
jdbc.username=root
jdbc.password=root

步骤2:修改src/config/JdbcConfig.java

@Configuration
//使用@PropertySource读取属性文件,值为属性文件路径加名称
//classpath为关键字,表明属性文件在类路径下
//ignoreResourceNotFound用于设置当属性文件未找到时使用的策略,
//值为true时,spring没找到属性文件会忽略,
//值为false时,spring没找到属性文件会抛出异常
@PropertySource("classpath:jdbc.properties",ignoreResourceNotFound = true)
public class JdbcConfig {
    //使用@Value注解通过spring的EL表达式获取属性文件中的值
    //表达式为属性文件中的key值
    //注意:读取属性文件使用$,而使用SpEL则使用的是#
    @Value("${jdbc.driver}")
    private String driver;
    @Value("${jdbc.url}")
    private String url;
    @Value("${jdbc.username}")
    private String username;
    @Value("${jdbc.password}")
    private String password;

    @Bean(name="dataSource")
    public DataSource getDataSource(){
        Properties p=new Properties();
        p.setProperty("driver",driver);
        p.setProperty("url",url);
        p.setProperty("username",username);
        p.setProperty("password",password);
        DataSource ds=null;
        try {
            ds= BasicDataSourceFactory.createDataSource(p);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return ds;
    }
}

装配的混合使用

对bean的装配,使用XML和注解都可实现。在实际开发过程中,应如何选择?

从注解角度出发来说,非第三方类(即自己开发的类),使用注解能够极大简化程序开发,而一旦使用了第三方类,注解开发其实并没有带来方便和快捷,反而出现了层次不明,类与类关系不清晰的问题。

因此,一般当公司无特定要求(规定使用纯注解或纯XML)时,建议使用混合装配:即自己开发的类尽量使用注解,对于第三方的类,尽量使用XML。

AOP

AOP:Aspect Oriented Programming的缩写,意为:面向切面编程,可以通过预编译方式和运行期动态代理,实现在不修改源代码的情况下给程序动态统一添加功能的一种技术。

AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

使用JDK动态代理理解AOP

jdk动态代理是java.lang.reflect.*包提供的方式,必须借助接口或子类才能产生代理对象。

需求:模拟业务层日志功能。

不使用动态代理

步骤1:创建业务层接口及实现类

UserService.java

public interface UserService {
    void save();
    void delete();
}

UserServiceImpl.java

@Service("userService")
public class UserServiceImpl implements UserService {
    @Autowired
    private CustomInteceptor customInteceptor;
    @Override
    public void save() {
        customInteceptor.beforeLog();
        try {
            int i=1/0;
            System.out.println("执行保存业务逻辑");
            customInteceptor.afterReturningLog();
        } catch (Exception e) {
            e.printStackTrace();
            customInteceptor.afterThrowingLog();
        } finally {
            customInteceptor.afterLog();
        }
    }

    @Override
    public void delete() {
        System.out.println("执行删除业务逻辑");
    }
}

步骤2:创建拦截器接口及实现类

CustomInteceptor.java

package com.woniu.inteceptor;

public interface CustomInteceptor {
    /**
     * 方法执行前记录日志
     */
    void beforeLog();

    /**
     * 方法执行后记录日志
     */
    void afterLog();

    /**
     * 方法正常返回后记录日志
     */
    void afterReturningLog();

    /**
     * 方法异常后记录日志
     */
    void afterThrowingLog();
}

MyCustomInteceptor.java

@Component
public class MyCustomInteceptor implements CustomInteceptor {
    @Override
    public void beforeLog() {
        System.out.println("记录日志之前的准备工作......");
    }

    @Override
    public void afterLog() {
        System.out.println("日志记录完成的善后工作......");
    }

    @Override
    public void afterReturningLog() {
        System.out.println("日志记录顺利完成了......");
    }

    @Override
    public void afterThrowingLog() {
        System.out.println("日志记录不幸出错了......");
    }
}

步骤5:修改测试类

public class SpringTest {
    private ApplicationContext applicationContext;
    @Before
    public void init(){
        applicationContext=new ClassPathXmlApplicationContext("applicationContext.xml");
    }

    @Test
    public void test(){
        UserService userService = applicationContext.getBean("userService", UserService.class);
        userService.save();
    }
}

运行测试类,可以看到MyCustomInteceptor类的方法在UserServiceImpl对应方法调用时都会被执行。

值得注意的是,在业务层实现类中出现了大量的代码冗余:每个方法中都有记录日志的代码和大量的try...catch...finally,如果还需要控制事务和权限的话,业务层每个方法中也会出现大量冗余的事务控制代码和权限控制代码。

而且在业务层中,我们应该只关心其具体业务实现,而不应将注意力分散到日志、事务和权限控制上去。此时的业务出现了大量的日志、事务和权限控制等与具体业务无关的代码,造成业务层职责不明确的情况出现。

可以使用动态代理来解决以上问题。

使用动态代理

步骤1:创建代理类及代理工厂类

ProxyBeanUtil.java

public class ProxyBeanUtil implements InvocationHandler {
    //真实对象
    private Object obj;
    //拦截器对象
    private Interceptor interceptor=null;
	//静态方法,根据真实对象和拦截器获取代理对象
    public static Object getBean(Object obj, Interceptor interceptor){
        ProxyBeanUtil _this=new ProxyBeanUtil();
        _this.obj=obj;
        _this.interceptor=interceptor;
        return Proxy.newProxyInstance(obj.getClass().getClassLoader(),obj.getClass().getInterfaces(),_this);
    }
	
    //代理方法:对真实对象的方法进行增强
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //真实方法返回对象
        Object retObj=null;
        //执行过程是否产生异常
        boolean exceptionFlag=false;
        //调用拦截器before()
        interceptor.before(obj);
        try {
            //反射激活真实对象的方法
            retObj = method.invoke(obj, args);
        } catch (Exception e) {
            //有异常
            exceptionFlag=true;
        }finally {
            //调用拦截器after()
            interceptor.after(obj);
        }
        if (exceptionFlag) {//如果有异常,调用拦截器afterThrowing()
            interceptor.afterThrowing(obj);
        }else {//如果没异常,调用拦截器afterReturning()
            interceptor.afterReturning(obj);
        }
        return retObj;
    }
}

ProxyBeanFactory.java

public class ProxyBeanFactory {
    //根据传入的真实对象和拦截器创建代理对象
    public static <T>T getBean(T obj, Interceptor interceptor){
        return (T) ProxyBeanUtil.getBean(obj,interceptor);
    }
}

步骤2:修改业务层实现类UserServiceImpl.java

@Service("userService")
public class UserServiceImpl implements UserService {
    @Autowired
    private CustomInteceptor customInteceptor;
    @Override
    public void save() {
        int i=1/0;
        System.out.println("执行保存业务逻辑");
    }

    @Override
    public void delete() {
        System.out.println("执行删除业务逻辑");
    }
}

步骤3:修改测试类:

public class SpringTest {
    private ApplicationContext applicationContext;
    @Before
    public void init(){
        applicationContext=new ClassPathXmlApplicationContext("applicationContext.xml");
    }

    @Test
    public void test(){
        UserService userService = applicationContext.getBean("userService", UserService.class);
        CustomInteceptor myCustomInteceptor = applicationContext.getBean("myCustomInteceptor", CustomInteceptor.class);
        //获取代理对象
        //类中使用自动注入后,该类不能通过new创建对象,只能通过spring容器获取对象,否则自动注入失效。
        //UserService proxy = ProxyBeanFactory.getBean(new UserServiceImpl(), new MyCustomInteceptor());
        UserService proxy = ProxyBeanFactory.getBean(userService, myCustomInteceptor);
        proxy.save();
        proxy.delete();
    }
}

运行测试类,可以看到在真实对象的方法执行之前,拦截器对象的before()、after()、afterReturning()方法都被执行了,而且业务层中不再出现冗余代码。

实际上,spring的AOP技术就是使用动态代理实现了以上功能。

AOP术语

连接点(join point) :能被拦截器拦截的方法。

切点(pointcut) :真正被拦截的方法。

注意:切点一定是连接点,而连接点不一定是切点,因为连接点有可能不被拦截,从而不进行增强。

通知(advice) :拦截器中的方法。分为前置通知,后置通知(也称最终通知),返回通知,异常通知,环绕通知五类。以反射执行目标对象方法为基准:

  • 前置通知:在目标对象方法执行之前执行;
  • 后置通知(最终通知):在目标对象方法执行完成后,无论在执行过程中是否出现异常,都会执行;
  • 异常通知:目标对象方法执行过程中产生异常后执行;
  • 返回通知:目标对象方法执行过程中不产生异常时执行;
  • 环绕通知:特殊的通知,不以目标对象方法为基准,可以同时实现前置和后置通知。环绕通知保留了调度目标对象原有方法的功能,也就是说,使用环绕通知甚至可以不必要调用目标对象方法,可以取代目标对象的方法。因此环绕通知非常强大,而且灵活,但可控性差,一般不需要大量改变业务逻辑的情况下,不会使用环绕通知。

切面(aspect) :通知与切点的结合。

织入(weaving) :将通知组合到目标的过程。

引入(introduction) :允许在现有类中添加自定义的类和方法。

纯XML开发spring AOP

需求:对使用动态代理完成增强的项目进行修改,使用spring AOP实现相同功能(配置纯XML实现)。

注:使用spring AOP模块功能,不但要在项目中导入spring-aop,还必须导入spring AOP依赖的aspectjweaver包,该包用于解析切点表达式。

另外,在案例中需要查询数据库表获取数据,需要使用spring内置数据源DriverManagerDataSource和spring封装jdbc操作的JdbcTemplate,这两个类都在spring-jdbc包中,因此需要导入对应jar包。

步骤1:导入jar包(spring-aop、aspectjweaver、spring-jdbc)。

步骤2:修改数据访问层接口实现类和业务层接口实现类

SubjectDaoImpl.java

public class SubjectDaoImpl implements SubjectDao {

    private JdbcTemplate jdbcTemplate;

    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @Override
    public Subject findBySubNo(int subNo) {
        List<Subject> subs = jdbcTemplate.query("select * from t_subject where sub_no=?",
                new BeanPropertyRowMapper<Subject>(Subject.class), subNo);
        return subs.isEmpty()?null:subs.get(0);
    }

    @Override
    public void addSubject(Subject subject) {
        jdbcTemplate.update("insert into t_subject(sub_name) values(?)", subject.getSubName());
    }

    @Override
    public List<Subject> findAll() {
        List<Subject> subs = jdbcTemplate.query("select * from t_subject",
                new BeanPropertyRowMapper<Subject>(Subject.class));
        return subs;
    }
}

注:JdbcTemplate封装了jdbc操作,查询时使用query()方法,增删改都使用update()方法。两个方法都有重载,可根据需求选择合适的来使用。

查询:query(String sql,RowMapper rowMapper,Object..args)

RowMapper是接口,spring提供了便利的RowMapper实现:BeanPropertyRowMapper。

BeanPropertyRowMapper类会先将其泛型指定的类实例化,再通过名称匹配的方式,将查询结果映射到属性中去。

增、删、改:update(String sql, Object..args)

SubjectServiceImpl.java

public class SubjectServiceImpl implements SubjectService {

    private SubjectDao subjectDao;

    public void setSubjectDao(SubjectDao subjectDao) {
        this.subjectDao = subjectDao;
    }

    @Override
    public Subject findBySubNo(int subNo) {
        return subjectDao.findBySubNo(subNo);
    }

    @Override
    public void addSubject(Subject subject) {
        subjectDao.addSubject(subject);
    }

    @Override
    public List<Subject> findAll() {
        //测试异常通知
//        int num=1/0;
        return subjectDao.findAll();
    }
}

步骤3:创建jdbc.properties,将jdbc操作的四个属性保存在文件中,实现软编码。

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/taotao
jdbc.username=root
jdbc.password=root

步骤4:修改applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>
<!-- 需要aop约束,可去core.html中查找获取 -->
<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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">
    
    <!-- spring加载属性文件,可以同时加载多个,使用","隔开,
		每个属性文件前都加classpath:,代表从类路径下获取,
		属性文件中的内容会被加载到spring容器,因此只要被spring容器管理的bean都可以拿到属性文件中的值
		通过@Value("属性文件中的key")
 	-->
    <context:property-placeholder location="classpath:jdbc.properties"/>

    <!-- 配置数据源,由于测试在main方法中进行,因此不能使得jndi数据源 -->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="${jdbc.driver}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>

	<!-- 配置jdbcTemplate -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"/>
    </bean>

	<!-- 配置数据访问层接口实现类 -->
    <bean id="subjectDao" class="com.wsjy.spring.dao.impl.SubjectDaoImpl">
        <property name="jdbcTemplate" ref="jdbcTemplate"/>
    </bean>
	
    <!-- 配置业务层接口实现类 -->
    <bean id="subjectService" class="com.wsjy.spring.service.impl.SubjectServiceImpl">
        <property name="sd" ref="subjectDao"/>
    </bean>
    
	<!-- 配置拦截器 -->
    <bean id="logger" class="com.wsjy.spring.interceptor.SubjectInterceptor"/>
    
	<!-- 配置AOP -->
    <aop:config>
        <!-- 配置切点 -->
        <aop:pointcut id="subjectInterceptor" expression="execution(* com.wsjy.spring.service.impl.*.*(..))"/>
        <!-- 配置切面 -->
        <aop:aspect ref="logger">
            <!-- 配置通知 -->
            <aop:before method="beforeLog" pointcut-ref="subjectInterceptor"/>
            <aop:after method="afterLog" pointcut-ref="subjectInterceptor"/>
            <aop:after-returning method="afterReturningLog" pointcut-ref="subjectInterceptor"/>
            <aop:after-throwing method="afterThrowingLog" pointcut-ref="subjectInterceptor"/>
        </aop:aspect>
    </aop:config>

</beans>

步骤5:修改测试类

public class Test {
	public static void main(String[] args) {
        //1、获取容器
    	ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
        //2、执行操作
        SubjectService ss = ac.getBean("subjectService", SubjectServiceImpl.class);
        List<Subject> subs = ss.findAll();
        System.out.println(subs);
}

运行测试类,可以看到代码执行结果与手动写动态代理实现效果相同。

AOP配置详解:

<!-- 配置AOP -->
 <aop:config>
     <!-- 
			aop:pointcut配置切点:建立拦截器与切点方法之间的关联,该标签只能出现在aop:aspect之前
			id属性表示切点名称,方便被通知引用
			expression属性用于设置切点表达式,明确的告诉spring,拦截器应对哪些切点进行增强
			切入点表达式的标准写法:
			访问修饰符 返回值 全限定类名.方法名(参数列表)
			实际使用时,访问修饰符可以省略不写。
			即“返回值 全限定类名.方法名(参数列表)”可以实现相同效果。
			返回值、全限定类名中的包名和类名、方法名、参数列表都可以使用*进行通配。
			返回值为*,表示任意返回值。
			包名有几级就写几个*,在表示每一级包名的*之间使用.隔开,表示任意包。
			也可以用*..来表示任意包及其子包。
			类名为*,表示任意类。
			方法名为*,表示任意方法。
			参数列表为*,表示任意类型的参数(必须有参数),也可以使用..表示任意个任意类型参数。
			根据以上规则,可以写一个通用切点表达式:  
			* *..*.*(..)   表示匹配任意包下任意类的有任意返回值且可传任意个参数的任意方法
			实际开发中,不会使用全通配,而是切到业务层实现类所在包,其余部分则可以使用通配符解决。
		-->
     <aop:pointcut id="subjectInterceptor" expression="execution(* com.wsjy.spring.service.impl.*.*(..))"/>
     <!-- 
			aop:aspect配置切面 
			选配id属性,表示切面名称
			必配ref属性,表示引用哪个拦截器来对切点方法进行增强
		-->
     <aop:aspect ref="logger">
         <!-- 
				aop:before、aop:after、aop:after-returning、aop:after-throwing配置通知
				method属性用于对切点进行增强的方法名
				pointcut属性用于指定切点
				pointcut-ref用于引用切点:如果增强方法过多,切点表达式可能会需要写多次,
				此时可使用aop:pointcut将切点表达式放到切面以外,
				需要使用时通过pointcut-ref属性指向aop:pointcut的id属性即可引用。
			-->
         <aop:before method="beforeLog" pointcut-ref="subjectInterceptor"/>
         <aop:after method="afterLog" pointcut-ref="subjectInterceptor"/>
         <aop:after-returning method="afterReturningLog" pointcut-ref="subjectInterceptor"/>
         <aop:after-throwing method="afterThrowingLog" pointcut-ref="subjectInterceptor"/>
     </aop:aspect>
 </aop:config>

纯注解开发spring AOP

需求:使用纯注解完成spring AOP项目开发。

步骤1:修改数据访问层接口实现类和业务层接口实现类

SubjectDaoImpl.java

@Repository("subjectDao")
public class SubjectDaoImpl implements SubjectDao {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public Subject findBySubNo(int subNo) {
        List<Subject> subs = jdbcTemplate.query("select * from t_subject where sub_no=?",
                new BeanPropertyRowMapper<Subject>(Subject.class), subNo);
        return subs.isEmpty()?null:subs.get(0);
    }

    @Override
    public void addSubject(Subject subject) {
        jdbcTemplate.update("insert into t_subject(sub_name) values(?)", subject.getSubName());
    }

    @Override
    public List<Subject> findAll() {
        List<Subject> subs = jdbcTemplate.query("select * from t_subject",
                new BeanPropertyRowMapper<Subject>(Subject.class));
        return subs;
    }
}

jdcbTemplate实例的获取有两种途径:

  • 自动注入:XML配置开发和注解开发两种都可以;
  • 继承JdbcDaoSupport.java:主要用于XML配置开发,注解开发受限。因为该类是继承的类,不是自己写的,无法使用注解实现自动注入。

SubjectServiceImpl.java

@Service("subjectService")
public class SubjectServiceImpl implements SubjectService {
    @Autowired
    private SubjectDao subjectDao;

    @Override
    public Subject findBySubNo(int subNo) {
        return subjectDao.findBySubNo(subNo);
    }

    @Override
    public void addSubject(Subject subject) {
        subjectDao.addSubject(subject);
    }

    @Override
    public List<Subject> findAll() {
        //测试异常通知
//        int num=1/0;
        return subjectDao.findAll();
    }
}

步骤2:修改jdbc配置类JdbcConfig.java

@Configuration
@PropertySource(value = "classpath:jdbc.properties",ignoreResourceNotFound = true)
public class JdbcConfig {
    @Value("${jdbc.driver}")
    private String driver;
    @Value("${jdbc.url}")
    private String url;
    @Value("${jdbc.username}")
    private String username;
    @Value("${jdbc.password}")
    private String password;
	//需要为数据访问层接口实现类注入JdbcTemplate
    @Bean(name = "jdbcTemplate")
    public JdbcTemplate getJdbcTemplate(){
        Properties p=new Properties();
        p.setProperty("driver",driver);
        p.setProperty("url",url);
        p.setProperty("username",username);
        p.setProperty("password",password);
        DataSource ds = null;
        try {
            ds = BasicDataSourceFactory.createDataSource(p);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return new JdbcTemplate(ds);
    }
}

步骤3:修改SubjectInterceptor.java,将其变为一个切面类。

@Component
//被@Aspect注解的类,spring都会看作切面类
@Aspect
public class SubjectInterceptor{
    //被@Pointcut注解的无逻辑方法用于配置切入点表达式,
    //方法名自定义,引用切入点表达式时使用“方法名称()”
    @Pointcut("execution(* com.wsjy.spring.service.impl.*.*(..))")
    public void pointCut(){}
    
	//配置前置通知,注解参数为切入点表达式,使用@Pointcut注解的方法名称+()
    @Before("pointCut()")
    public void beforeLog() {
        System.out.println("准备获取Subject对象");
    }
	//配置后置通知
    @After("pointCut()")
    public void afterLog() {
        System.out.println("已经获取Subject对象");
    }
	//配置返回通知
    @AfterReturning("pointCut()")
    public void afterReturningLog() {
        System.out.println("刚刚获取Subject对象,一切正常");
    }
	//配置异常通知
    @AfterThrowing("pointCut()")
    public void afterThrowingLog() {
        System.out.println("获取Subject对象过程中出现了异常");
    }
}

步骤4:创建AOP配置类AopConfig.java

@Configuration
@EnableAspectJAutoProxy
@ComponentScan("com.wsjy.spring.interceptor")
public class AopConfig {
    @Bean(name="subjectInterceptor")
    public SubjectInterceptor getSubjectIntercetor(){
        return new SubjectInterceptor();
    }
}

注:@EnableAspectJAutoProxy用于启用AspectJ框架自动代理,这样spring才会生成动态代理对象,从而可以使用AOP。

getSubjectIntercetor()用于生成一个切面实例。

步骤5:修改主配置类ApplicationConfig.java

@Configuration
@ComponentScan(basePackages = {"com.wsjy"})
@Import({JdbcConfig.class,AopConfig.class})
public class ApplicationConfig {

}

步骤6:修改测试类

public class Test {
	public static void main(String[] args) {
        //1、获取容器
    	ApplicationContext ac = new AnnotationConfigApplicationContext(ApplicationConfig.class);
        //2、执行操作
        SubjectService ss = ac.getBean("subjectService", SubjectService.class);
        List<Subject> subs = ss.findAll();
        System.out.println(subs);
}

运行测试类代码,可得到与XML配置相同的结果。

XML+注解开发spring AOP

纯注解或纯XML开发spring项目都有各自的不足,一般实现开发过程中会使用XML+注解的方式完成spring项目的开发。

一般来说,XML会用于配置第三方的类,因为XML配置层次结构清晰,便于理解;而自己开发的类则会使用注解来简化开发过程。

需求:使用XML+注解方式完成spring AOP项目开发。

步骤1:修改核心配置文件applicationContext.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"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="com.wsjy"/>

    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/taotao"/>
        <property name="username" value="root"/>
        <property name="password" value="root"/>
    </bean>

    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <aop:aspectj-autoproxy/>
</beans>

步骤2:修改测试类

public class Test {
	public static void main(String[] args) {
        //1、获取容器
    	ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
        //2、执行操作
        SubjectService ss = ac.getBean("subjectService", SubjectService.class);
        List<Subject> subs = ss.findAll();
        System.out.println(subs);
}

由于使用了XML文件做核心配置,因此,纯注解开发中的三个配置类:ApplicationConfig.java、JdbcConfig.java、AopConfig.java都可以不要,但切面类上需要增加一个@Component注解,否则spring扫描不到,AOP效果出不来。

@Component
@Aspect
public class SubjectInterceptor{
 ......
}

除此以外,其余代码无需修改,此时运行测试类得到的结果与纯XML开发和纯注解开发的结果是一样的。

引入

spring AOP只是通过动态代理技术,把各类通知织入到所约定的流程中,而事实上,有时候希望通过引入其他类的方法来得到更好的实现,此时可以通过“引入(introduction)”来得到其他类的方法。

需求:对于SubjectServiceImpl.java中的addSubject(Subject subject),需要检测传递的参数是否为空,如果为空时,不执行向数据库新增的操作。

步骤1:创建验证Subject对象是否为空的接口及其实现类

SubjectVerifier.java

public interface SubjectVerifier {
    /**
     * 验证参数是否为空
     * @param subject
     * @return
     */
    boolean verify(Subject subject);
}

SubjectVerifierImpl.java

public class SubjectVerifierImpl implements SubjectVerifier {

    @Override
    public boolean verify(Subject subject) {
        return subject!=null;
    }
}

步骤2:修改切面类SubjectInterceptor.java

@Component
@Aspect
public class SubjectInterceptor{
    //@DeclareParents用于引入某个类,
    //value属性指定增强的对象,在此例中就是为SubjectServiceImpl引入一个新的接口实现对其的增强
    //defaultImpl属性指定默认的实现类
    @DeclareParents(value = "com.wsjy.spring.service.impl.SubjectServiceImpl+",
            defaultImpl = SubjectVerifierImpl.class)
    private SubjectVerifier subjectVerifier;

    @Pointcut("execution(* com.wsjy.spring.service.impl.*.*(..))")
    public void pointCut(){}

    @Before("pointCut()")
    public void beforeLog() {
        System.out.println("准备获取Subject对象");
    }

    @After("pointCut()")
    public void afterLog() {
        System.out.println("已经获取Subject对象");
    }

    @AfterReturning("pointCut()")
    public void afterReturningLog() {
        System.out.println("刚刚获取Subject对象,一切正常");
    }

    @AfterThrowing("pointCut()")
    public void afterThrowingLog() {
        System.out.println("获取Subject对象过程中出现了异常");
    }
}

步骤3:修改测试类

public class Test {
	public static void main(String[] args) {
        //1、获取容器
    	ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
        //2、执行操作
        SubjectService ss = ac.getBean("subjectService", SubjectService.class);
        Subject subject=new Subject();
        subject.setSubName("mybatis");
        //测试对象为空时是否执行向数据库新增
 		//subject=null;
        SubjectVerifier sv= (SubjectVerifier) ss;
        if (sv.verify(subject)) {
            System.out.println("subject is not null");
            ss.addSubject(subject);
        }else{
            System.out.println("subject is null");
        }
}

运行测试类,可以看到:

  • 当Subject实例不为空时,执行了向数据库新增的操作,并且AOP功能也同步执行了;
  • 当Subject实例为空时,没有执行新增操作,AOP也不执行。

注解配置AOP时通知的执行顺序问题

一般在实际开发过程,spring AOP主要用于日志记录、权限控制和事务处理,而使用注解实现spring AOP时,通知的执行顺序是前置-->后置-->返回(异常),此时就会带来无法控制事务的问题(XML实现不会出现)。

验证XML实现spring AOP事务控制

需求:实现转账功能(使用XML实现spring AOP事务控制)。

注意:要实现事务控制,不能使用JdbcTemplate。查看源码可知:JdbcTemplate每次进行操作都会从数据库连接池中获取一个新的连接,因此无法控制执行事务的数据库连接是同一个,事务控制一定会失败。

要实现效果,需要保证执行事务操作的连接是同一个,可选择使用commons-dbutils包中的QueryRunner来实现,QueryRunner组件可以在执行操作时传递一个数据库连接,只要保证每次传递的数据库连接是同一个,则能保证事务控制成功。

保证所有操作使用同一个数据库连接,可使用ThreadLocal将数据库连接绑定到线程中,此时同一个线程操作会使用同一个数据库连接。

另外还要注意一点,在业务层进行事务处理时,不能够在业务层处理异常,而应该将异常继续上抛,否则业务层中处理了异常,则事务无法在异常时回滚,同样也会造成事务控制失败。

步骤1:在数据库中创建t_account表,有aid、userName和balance字段,添加数据。

步骤2:创建web项目,在WEB-INF下创建lib目录,导入spring IoC(spring-core、spring-context、spring-jcl、spring-expression、spring-beans)和spring Aop(spring-aop、aspectjweaver)。此外,此项目的JDBC操作使用commons-dbutils,数据源从spring数据库连接池(存在于spring-jdbc)中获取,此项目会连接数据库查询数据,因此还需要mysql驱动包。

步骤3:创建类和接口

实体类:Account.java

public class Account {
    private Long aid;
    private String userName;
    private Double balance;

    /*setter/getter/toString*/
}

工具类:ConnectionUtils.java

/**
 * 事务控制在service层处理,而数据库连接只在dao层出现,
 * 为了解决这个矛盾,可以数据库连接绑定到线程上。
 * 因为在web项目中,一个请求是通过一个线程来处理。
 * 也就是说,对于三层架构的项目来说,dao层、service层、controller层使用的是同一个线程。
 * 因此当连接被绑定到线程上时,在service层就可以得到数据库连接,进行事务控制。
 */
public class ConnectionUtils {
    //单例实现ThreadLocal对象
    private static ThreadLocal<Connection> tl=new ThreadLocal<Connection>();
	//数据源
    private DataSource dataSource;

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public Connection getThredLocalConnection(){
        try {
            //业务层需要数据库连接时,先从线程中取
            Connection conn = tl.get();
            //如果取到的连接为null,从连接池中获取一个。
            if (conn == null) {
                conn = dataSource.getConnection();
                //将获取的连接绑定到线程上
                tl.set(conn);
            }
            return conn;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public void unbindThreadLocalConnection(){
        //JDBC操作完成需要释放资源时,将线程上的连接移除
        tl.remove();
    }
}

事务管理类:TransactionManager.java

public class TransactionManager {
    private ConnectionUtils connectionUtils;

    public void setConnectionUtils(ConnectionUtils connectionUtils) {
        this.connectionUtils = connectionUtils;
    }
	//开启事务
    public void beginTransaction(){
        try {
            connectionUtils.getThredLocalConnection().setAutoCommit(false);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
	//提交事务
    public void commit(){
        try {
            connectionUtils.getThredLocalConnection().commit();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
	//回滚事务
    public void rollback(){
        try {
            connectionUtils.getThredLocalConnection().rollback();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
	//释放资源
    public void close(){
        try {
            //将连接还回连接池
            connectionUtils.getThredLocalConnection().close();
            //将线程上的连接移除
            connectionUtils.unbindThreadLocalConnection();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

数据访问层接口:AccountDao.java

public interface AccountDao {
    /**
     * 根据id查询账户信息
     * @param aid
     * @return
     */
    Account findById(int aid);
    /**
     * 根据用户名查询账户信息
     * @param userName
     * @return
     */
    Account findByName(String userName);

    /**
     * 修改账户信息
     * @param account
     */
    void updateAccount(Account account);
}

数据访问层接口实现类:AccountDaoImpl.java

public class AccountDaoImpl implements AccountDao {
	//用于进行JDBC操作的对象,该对象将在容器中配置,由spring容器注入
    private QueryRunner query;

    public void setQuery(QueryRunner query) {
        this.query = query;
    }

    private ConnectionUtils connectionUtils;

    public void setConnectionUtils(ConnectionUtils connectionUtils) {
        this.connectionUtils = connectionUtils;
    }


    @Override
    public Account findById(int aid) {
        try {
            //与jdbcTemplate类似,调用query()方法进行查询
            //结果集为单个,使用BeanHandler封装
            Account account = query.query(connectionUtils.getThredLocalConnection(),
                    "select * from t_account where aid=?",
                    new BeanHandler<Account>(Account.class), aid);
            return account;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public Account findByName(String userName) {
        try {
            //结果集为集合(多个),使用BeanListHandler封装
            List<Account> accounts = query.query(connectionUtils.getThredLocalConnection(),
                    "select * from t_account where userName=?",
                    new BeanListHandler<Account>(Account.class), userName);
            if (accounts.isEmpty()) {
                return null;
            }
            if (accounts.size() > 1) {
                throw new RuntimeException("结果集不唯一,数据有问题");
            }
            return accounts.get(0);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public void updateAccount(Account account) {
        try {
            //更新
            query.update(connectionUtils.getThredLocalConnection(),
                    "update t_account set balance=? where userName=?",
                    account.getBalance(), account.getUserName());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

业务层接口:AccountService.java

public interface AccountService {
    /**
     * 根据id查询账户信息
     * @param aid
     * @return
     */
    Account findById(int aid);

    /**
     * 根据用户名称查询账户信息
     * @param userName
     * @return
     */
    Account findByName(String userName);

    /**
     * 转账操作
     * @param sourceUserName
     * @param targetUserName
     * @param transferAmount
     */
    void transfer(String sourceUserName,String targetUserName,Double transferAmount);
}

业务层接口实现类:AccountServiceImpl.java

public class AccountServiceImpl implements AccountService {
    private AccountDao accountDao;

    public void setAccountDao(AccountDao accountDao) {
        this.accountDao = accountDao;
    }

    @Override
    public Account findById(int aid) {
        Account account = accountDao.findById(aid);
        return account;
    }

    @Override
    public Account findByName(String userName) {
        Account account = accountDao.findByName(userName);
        return account;
    }

    @Override
    public void transfer(String sourceUserName, String targetUserName, Double transferAmount) {
        Account source = accountDao.findByName(sourceUserName);
        Account target = accountDao.findByName(targetUserName);

        source.setBalance(source.getBalance()-transferAmount);
        target.setBalance(target.getBalance()+transferAmount);

        accountDao.updateAccount(source);
        //测试异常通知
        //int i=1/0;
        accountDao.updateAccount(target);
    }
}

步骤4:创建核心配置文件applicationContext.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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">
    <!-- 配置service -->
    <bean id="accountService" class="com.wsjy.service.impl.AccountServiceImpl">
        <property name="accountDao" ref="accountDao"/>
    </bean>
	<!-- 配置事务管理器 -->
    <bean id="transactionManager" class="com.wsjy.utils.TransactionManager">
        <property name="connectionUtils" ref="connectionUtils"/>
    </bean>
	<!-- 配置工具类 -->
    <bean id="connectionUtils" class="com.wsjy.utils.ConnectionUtils">
        <property name="dataSource" ref="dataSource"/>
    </bean>
	<!-- 配置dao -->
    <bean id="accountDao" class="com.wsjy.dao.impl.AccountDaoImpl">
        <property name="query" ref="query"/>
        <property name="connectionUtils" ref="connectionUtils"/>
    </bean>
	<!-- 配置QueryRunner -->
    <bean id="query" class="org.apache.commons.dbutils.QueryRunner"></bean>
	<!-- 配置数据源 -->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/taotao"/>
        <property name="username" value="root"/>
        <property name="password" value="root"/>
    </bean>
	<!-- 配置AOP -->
    <aop:config>
        <!-- 配置公共的切入点表达式 -->
        <aop:pointcut id="pt" expression="execution(* com.wsjy.service.impl.*.*(..))"/>
        <!-- 配置切面 -->
        <aop:aspect ref="transactionManager">
            <!-- 配置前置通知 -->
            <aop:before method="beginTransaction" pointcut-ref="pt"/>
            <!-- 配置返回通知 -->
            <aop:after-returning method="commit" pointcut-ref="pt"/>
            <!-- 配置异常通知 -->
            <aop:after-throwing method="rollback" pointcut-ref="pt"/>
            <!-- 配置后置通知 -->
            <aop:after method="close" pointcut-ref="pt"/>
        </aop:aspect>
    </aop:config>
</beans>

步骤5:创建测试类

public class Test {
    public static void main(String[] args) {
        ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
        AccountService as = ac.getBean("accountService", AccountService.class);
        as.transfer("张三","李四",200.00);
    }
}

通过注释或放开"int i=1/0;"可以测试数据库事务是否生效,运行测试类:

  • 当"int i=1/0;"被注释时,转账成功,数据库表中的数据更新正常;
  • 当"int i=1/0;"没有被注释时,转账失败,因为有异常出现,此时异常通知被执行,执行回滚操作,数据库表中的数据一致性得到保障。

验证注解实现spring AOP事务控制

在上例基础上,使用注解装配核心配置文件中自己写的类。

步骤1:修改dao层实现类AccountDaoImpl.java

<?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:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">
	<!-- 配置spring容器启动时扫描的包 -->
    <context:component-scan base-package="com.wsjy"/>
	<!-- 配置QueryRunner -->
    <bean id="query" class="org.apache.commons.dbutils.QueryRunner"></bean>
	<!-- 配置数据源 -->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/taotao"/>
        <property name="username" value="root"/>
        <property name="password" value="root"/>
    </bean>
	<!-- 启用AOP -->
    <aop:aspectj-autoproxy/>
</beans>

步骤2:使用注解装配对应的类和接口

工具类:ConnectionUtils.java

@Component
public class ConnectionUtils {
    private static ThreadLocal<Connection> tl=new ThreadLocal<Connection>();
    @Autowired
    private DataSource dataSource;

    public Connection getThredLocalConnection(){
        try {
            Connection conn = tl.get();
            if (conn == null) {
                conn = dataSource.getConnection();
                tl.set(conn);
            }
            return conn;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public void unbindThreadLocalConnection(){
        tl.remove();
    }
}

事务管理类:TransactionManager.java

@Component
@Aspect
public class TransactionManager {
    @Autowired
    private ConnectionUtils connectionUtils;
    //定义切入点表达式
    @Pointcut("execution(* com.wsjy.service.impl.*.*(..))")
    private void pointCut(){}

    @Before("pointCut()")
    public void beginTransaction(){
        ......
    }
    @AfterReturning("pointCut()")
    public void commit(){
        ......
    }
    @AfterThrowing("pointCut()")
    public void rollback(){
        ......
    }
    @After("pointCut()")
    public void close(){
        ......
    }
}

数据访问层接口实现类:AccountDaoImpl.java

@Repository("accountDao")
public class AccountDaoImpl implements AccountDao {
    //使用自动注入,无需setter方法
    @Autowired
    private QueryRunner query;

    @Autowired
    private ConnectionUtils connectionUtils;

    ......
}

业务层接口实现类:AccountServiceImpl.java

@Service("accountService")
public class AccountServiceImpl implements AccountService {
    @Autowired
    private AccountDao accountDao;
	
    ......
}

在某些低版本spring中,有可能出现以下异常:

  • 当“int i=1/0;”被注释,异常提示信息:Can't call commit when autocommit=true;
  • 当“int i=1/0;”没有被注释,异常提示信息:Can't call rollback when autocommit=true。此时如果在数据库环境下执行“commit;”语句,会发现异常出现前的更新已被执行,回滚失败,事务控制失败。

出现以上问题的原因是:

使用注解实现AOP时,通知的执行顺序是前置-->后置-->返回(异常),而后置通知中执行了释放资源和解除绑定的操作:

connectionUtils.getThredLocalConnection().close();
connectionUtils.unbindThreadLocalConnection();

此时再回头去执行返回(异常)通知:

//connectionUtils.getThredLocalConnection().rollback();
connectionUtils.getThredLocalConnection().commit();

此时会调用connectionUtils.getThredLocalConnection(),而这时的线程上的连接在后置通知中被关闭和移除,因此线程上没有连接,此时会重新创建一个连接并绑定到线程上:

conn = dataSource.getConnection();
tl.set(conn);

而新生成的连接默认是autocommit=true,因此就出现了以上的异常。

以上问题可以通过配置环绕通知来解决。

环绕通知

环绕通知是spring中最为强大和灵活的通知,使用它可以得到目标方法的完全控制权:控制目标方法是否执行、控制目标方法参数、控制目标方法返回值。

spring框架为我们提供了一个接口:proceedingJoinPoint。该接口有一个方法proceed(),此方法就相当于明确调用切入点方法。

环绕通知的使用:

修改事务管理类:TransactionManager.java

@Component
@Aspect
public class TransactionManager {
 @Autowired
 private ConnectionUtils connectionUtils;
 @Pointcut("execution(* com.wsjy.service.impl.*.*(..))")
 private void pointCut(){}

 public void beginTransaction(){
     try {
         connectionUtils.getThredLocalConnection().setAutoCommit(false);
     } catch (SQLException e) {
         e.printStackTrace();
     }
 }

 public void commit(){
     try {
         connectionUtils.getThredLocalConnection().commit();
     } catch (SQLException e) {
         e.printStackTrace();
     }
 }

 public void rollback(){
     try {
         connectionUtils.getThredLocalConnection().rollback();
     } catch (SQLException e) {
         e.printStackTrace();
     }
 }

 public void close(){
     try {
         connectionUtils.getThredLocalConnection().close();
         connectionUtils.unbindThreadLocalConnection();
     } catch (SQLException e) {
         e.printStackTrace();
     }
 }
 //配置环绕通知,手动控制通知执行顺序
 @Around("pointCut()")
 public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint){
     Object retValue=null;
     try {
         //获取目标方法的所有参数
         Object[] args=proceedingJoinPoint.getArgs();
         this.beginTransaction();
         //执行目标方法
         retValue = proceedingJoinPoint.proceed(args);
         this.commit();
         return retValue;
     } catch (Throwable e) {
         this.rollback();
         throw new RuntimeException(e);
     } finally {
         this.close();
     }
 }
}

运行测试类,测试异常和正常执行,执行结果正常。

声明式事务

在spring中,数据库事务可以使用声明式事务,也可以使用编程式事务。但编程式事务会造成大量代码冗余,代码可读性差,因此目前基本上都使用声明式事务。

spring的数据库事务是通过PlatformTransactionManager进行管理的,该接口下有两个常用的实现类:DataSourceTransactionManager和HibernateTransactionManager(专供Hibernate框架使用)。

对于spring来说,事务控制中的前置通知(开启事务)和后置通知(释放资源)属于公共部分,每个事务都需要进行对应操作,因此spring已经对其进行了封装,在PlatformTransactionManager接口中只提供了commit()和rollback(),用于对返回通知和异常通知进行处理。

spring声明式事务(XML)

需求:使用spring声明式事务进行事务控制(XML配置实现),修改之前的XML配置实现spring AOP转账案例即可。

使用XML实现spring声明式事务的步骤:

  1. 配置事务管理器;
  2. 配置事务通知:为业务层方法配置事务,并设置事务属性;
  3. 配置切面:配置切入点表达式,并让切入点表达式与事务通知产生关联;

步骤1:在XML实现spring AOP项目的的基础上导入spring-tx,该jar包是spring事务控制的依赖,同时本案例使用JdbcTemplate实现,commons-dbutils不再需要。

步骤2:删除ConnectionUtils.java和TransactionManager.java。

步骤3:修改dao层实现类AccountDaoImpl.java

public class AccountDaoImpl implements AccountDao {

    private JdbcTemplate jdbcTemplate;

    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @Override
    public Account findById(int aid) {
        try {
            List<Account> accounts = jdbcTemplate.query("select * from t_account where aid=?",
                    new BeanPropertyRowMapper<Account>(Account.class), aid);
            return accounts.isEmpty()?null:accounts.get(0);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public Account findByName(String userName) {
        try {
            List<Account> accounts = jdbcTemplate.query("select * from t_account where userName=?",
                    new BeanPropertyRowMapper<Account>(Account.class), userName);
            if (accounts.isEmpty()) {
                return null;
            }
            if (accounts.size() > 1) {
                throw new RuntimeException("结果集不唯一,数据有问题");
            }
            return accounts.get(0);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public void updateAccount(Account account) {
        try {
            jdbcTemplate.update("update t_account set balance=? where userName=?",
                    account.getBalance(), account.getUserName());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

步骤4:修改核心配置文件applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>
<!-- 要使用声明式事务,需要引入tx和aop约束,在data access中查找xmlns:tx即可 -->
<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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd
        http://www.springframework.org/schema/tx
        https://www.springframework.org/schema/tx/spring-tx.xsd">
	<!-- 配置业务层 -->
    <bean id="accountService" class="com.wsjy.service.impl.AccountServiceImpl">
        <property name="accountDao" ref="accountDao"/>
    </bean>
	<!-- 配置数据访问层 -->
    <bean id="accountDao" class="com.wsjy.dao.impl.AccountDaoImpl">
        <property name="jdbcTemplate" ref="jdbcTemplate"/>
    </bean>
	<!-- 配置jdbcTemplate -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"/>
    </bean>
	<!-- 配置数据源 -->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/taotao"/>
        <property name="username" value="root"/>
        <property name="password" value="root"/>
    </bean>
	<!-- 配置事务管理器 -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
	<!-- 配置事务通知:
		id:事务通知的唯一标识
		transaction-manager:引入事务管理器处理事务通知
 	-->
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <!-- 配置事务通知的属性
			isolation:设定事务隔离级别,用于保证数据的完整性。
				DEFAULT,默认使用数据库的事务隔离级别
				READ_UNCOMMITTED,可读取未提交数据,有可能出现脏读、不可重复读、幻读
				READ_COMMITTED,可读取已提交数据,有可能出现不可重复读、幻读
				REPEATABLE_READ,读取的数据表被加行锁,有可能出现幻读
				SERIALIZABLE,读取的数据表加表锁,安全程度最高
			propagation:当被事务控制的业务方法进行相互调用时,设定事务的传播行为。
				REQUIRED,默认,之前的操作中有事务时加入该事务,没有事务时创建一个事务(增删改);
				SUPPORTS,之前的操作中有事务时加入该事务,没有事务时不使用事务(查询)
				MANDATORY,必须在事务内部执行,没有事务就报异常
				REQUIRES_NEW,将原有事务挂起,新建一个事务执行自己的操作,两个事务之间没有关联
				NOT_SUPPORTED,必须在非事务内部执行,如果有事务存在,将事务挂起,执行自己的操作
				NEVER,不能在事务内部执行,有事务就报异常
				NESTED,之前的操作有事务时,创建一个嵌套事务,两个事务之间会产生关联
			read-only:设定事务是否只读。默认false,读写(增删改),true,只读(查询)
			timeout:设定事务的超时时间,默认-1,永不超时,设定数值时,以秒为单位计算 
 			no-rollback-for:
				设定一个异常,事务执行过程中出现该异常时不回滚,其它异常会回滚,不设默认全回滚
			rollback-for:
				设定一个异常,事务执行过程中出现该异常时回滚,其它异常不会回滚,不设默认全回滚
				建议手动抛异常时设定该属性
		-->
        <tx:attributes>
            <!-- 配置需要事务控制的方法
 				name属性指定业务层方法名,可使用*进行通配
				一般会使用*通配所有业务层方法,然后使用某些特定规则将查询方法单独标记
				比如下例中的*通配所有业务层方法,find*匹配以find开头的所有业务层方法,
				find*影响范围比*小,优先级比*高,不会造成执行异常。
				建议查询方法会使用固定方式进行命名。
			-->
            <tx:method name="*" propagation="REQUIRED" read-only="false"/>
            <tx:method name="find*" propagation="SUPPORTS" read-only="true"/>
        </tx:attributes>
    </tx:advice>
	<!-- 配置AOP -->
    <aop:config>
        <!-- 配置切入点表达式 -->
        <aop:pointcut id="pointCut" expression="execution(* com.wsjy.service.impl.*.*(..))"/>
        <!-- 建立切入点表达式与事务通知的关联 -->
        <aop:advisor advice-ref="txAdvice" pointcut-ref="pointCut"/>
    </aop:config>

</beans>

运行测试类,通过对transfer()方法中“int i=1/0;”的注释与放开,运行结果正常,事务控制成功实现。