Spring Framewor 简介

Spring5.0已经全面支持JDK8,建议JDK使用1.8版本。

Spring Framework是Spring家族中其他框架的底层基础。

Spring Framewor 架构

Spring Framework的发展经历了很多版本的变更,每个版本都有相应的调整。

Spring Framework 架构:

  1. 核心层

    Core Container:核心容器,这个模块是Spring最核心的模块,其他的都需要依赖该模块。

  2. AOP层

    • AOP:面向切面编程,它依赖核心层容器,目的是在不改变原有代码的前提下对其进行功能增强
    • Aspects:AOP是编程思想,Aspects是对AOP思想的具体实现。
  3. 数据层

    • Data Access:数据访问,Spring全家桶中有对数据访问的具体实现技术。
    • Data Integration:数据集成,Spring支持整合其他的数据层解决方案,比如Mybatis。
    • Transactions:事务,Spring中事务管理是Spring AOP的一个具体实现。
  4. Web层

  5. Test层

    Spring主要整合了Junit来完成单元测试和集成测试。

从Spring Framework 5没有架构图,而最新的架构图是4版本,所以可以认为Spring Framework从4版本开始架构就已经趋于稳定,没有什么变动。

Spring 核心概念

在Spring核心概念主要包含:

  • IOC

    • IOC,Inversion of Control,即控制反转。
    • 控制反转:主动new产生对象转换为由外部提供对象,此过程中对象创建控制权由程序转移到外部的一种思想。

    例如,业务(Service)层要使用到数据(DAO)层的类对象。此时就可以使用IOC思想,由外部程序给业务层创建数据层对象。 这样可以降低业务层和数据层之间的耦合性。 如果数据层的实现更改的话,就无需在业务层中修改实现类。

    • IOC容器
      • Spring提供了一个容器,称为IOC容器,用来充当IOC思想中的"外部"。
      • IOC容器负责对象的创建、初始化等一系列工作。
    • Bean
      • 在IOC容器中,被创建或被管理的对象统称为Bean
      • IOC容器中存放的就是一个个的Bean对象。
  • DI

    • 依赖注入:在容器中建立Bean与Bean之间的依赖关系的整个过程。

    例如,业务层和数据层在IOC容器中创建Bean后,并不能直接工作,因为业务层需要依赖数据层才能正确工作。所以此时就需要使用依赖注入,在业务层和数据层之间建立依赖关系。

IOC和DI的最终目标就是充分解耦。在Spring Framewor中的实现依靠:

  • 使用IOC容器管理Bean(IOC)。
  • 在IOC容器内将有依赖关系的Bean进行关系绑定(DI)。

最终,使用对象时不仅可以直接从IOC容器中获取,并且获取到的bean已经绑定了所有的依赖关系。


配置文件方法使用IOC/DI

使用 IOC 容器创建 Bean(bean 标签)

基础配置:

  1. 创建Maven项目,项目基础结构如下:

    • 📁project-file-name
      • 📁src
        • 📁main
          • 📁java
            • 📁com.linner
              • 📁dao
              • 📁domain
              • 📁service
          • 📁resourcs
        • 📁test
          • 📁java
            • 📁com.linner
              • 📁service
        • 📄pom.xml
  2. 添加Spring Framework的依赖jar包:

    <dependencies>
        <!--...-->
        <!--springframework-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.10.RELEASE</version>
        </dependency>
        <!--junit-->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <!--...-->
    </dependencies>
    
  3. 添加业务层、数据层类:

    • User.java:

      package com.linner.domain;
      
      public class User {
          Integer id;
          String username;
          String password;
      
          public Integer getId() {
              return id;
          }
      
          public void setId(Integer id) {
              this.id = id;
          }
      
          public String getUsername() {
              return username;
          }
      
          public void setUsername(String username) {
              this.username = username;
          }
      
          public String getPassword() {
              return password;
          }
      
          public void setPassword(String password) {
              this.password = password;
          }
      
          @Override
          public String toString() {
              return "User{" +
                      "id=" + id +
                      ", username='" + username + '\'' +
                      ", password='" + password + '\'' +
                      '}';
          }
      }
      
    • DAO(仅测试,无需查询数据库):

      接口:

      package com.linner.dao;
      
      import com.linner.domain.User;
      
      import java.util.List;
      
      public interface UserDao {
      
          List<User> findAll();
      
          User findById(int id);
      
          void delete(int id);
      
          void save(User user);
      
          void update(User user);
      }
      

      实现类:

      package com.linner.dao.impl;
      
      import com.linner.dao.UserDao;
      import com.linner.domain.User;
      
      import java.util.List;
      
      public class UserDaoImpl implements UserDao {
          @Override
          public List<User> findAll() {
              System.out.println("UserDao findAll...");
              return null;
          }
      
          @Override
          public User findById(int id) {
              System.out.println("UserDao findById:" + id);
              return null;
          }
      
          @Override
          public void delete(int id) {
              System.out.println("UserDao delete:" + id);
          }
      
          @Override
          public void save(User user) {
              System.out.println("UserDao save:" + user);
          }
      
          @Override
          public void update(User user) {
              System.out.println("UserDao update:" + user);
          }
      }
      
    • Service:

      接口:

      package com.linner.service;
      
      import com.linner.domain.User;
      
      import java.util.List;
      
      public interface UserService {
      
          List<User> findAll();
      
          User findById(int id);
      
          void delete(int id);
      
          void save(User user);
      
          void update(User user);
      }
      

      实现类:

      package com.linner.service.impl;
      
      import com.linner.dao.UserDao;
      import com.linner.dao.impl.UserDaoImpl;
      import com.linner.domain.User;
      import com.linner.service.UserService;
      
      import java.util.List;
      
      public class UserServiceImpl implements UserService {
          private UserDao userDao = new UserDaoImpl();
      
          @Override
          public List<User> findAll() {
              System.out.println("UserService findAll...");
              userDao.findAll();
              return null;
          }
      
          @Override
          public User findById(int id) {
              System.out.println("UserService findById:" + id);
              userDao.findById(id);
              return null;
          }
      
          @Override
          public void delete(int id) {
              System.out.println("UserService delete:" + id);
              userDao.delete(id);
          }
      
          @Override
          public void save(User user) {
              System.out.println("UserService save:" + user);
              userDao.save(user);
          }
      
          @Override
          public void update(User user) {
              System.out.println("UserService update:" + user);
              userDao.update(user);
          }
      }
      
  • 测试方法:

    package com.linner.service;
    
    import com.linner.domain.User;
    import com.linner.service.UserService;
    import com.linner.service.impl.UserServiceImpl;
    import org.junit.Test;
    
    import java.util.List;
    
    public class UserServiceTest {
        private UserService userService = new UserServiceImpl();
    
        @Test
        public void testFindAll() {
            userService.findAll();
            System.out.println("------------");
        }
    
        @Test
        public void testFindById() {
            int id = 12;
            userService.findById(id);
            System.out.println("------------");
        }
    
        @Test
        public void testDelete() {
            int id = 12;
            userService.delete(id);
            System.out.println("------------");
        }
    
        @Test
        public void testSave() {
            User user = new User();
            user.setId(12);
            user.setUsername("abc");
            user.setPassword("abc123");
            userService.save(user);
            System.out.println("------------");
        }
    
        @Test
        public void testUpdate() {
            User user = new User();
            user.setId(12);
            user.setUsername("abc");
            user.setPassword("abc123");
            userService.update(user);
            System.out.println("------------");
        }
    }
    

创建Bean

  • 在📁resourcs目录下创建String配置文件applicationContext.xml,并使用<bean>标签配置Bean:

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    
        <!--添加UserService-->
        <bean id="userService" class="com.linner.service.impl.UserServiceImpl"/>
    </beans>
    
  • 接着在程序中使用Spring提供的方法获取IOC容器。然后从IOC容器中获取对象并调用其方法,修改Test类进行测试:

    public class UserServiceTest {
    
        // private UserService userService = new UserServiceImpl();
    
        // 使用IOC无需自己创建对象(对象由IOC容器分配
        private static UserService userService;
        // 这里使用静态代码块获取容器和Bean
        static {
            // 获取IOC容器
            ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
            userService = (UserService) ctx.getBean("userService");
        }
    
        // ...
    }
    

    IOC核心容器创建方式有:

    • ClassPathXmlApplicationContext:类路径下的XML配置文件。

      ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
      

      ClassPathXmlApplicationContext的参数为XML配置文件在类路径下的相对路径。

    • FileSystemXmlApplicationContext

      ApplicationContext ctx = new FileSystemXmlApplicationContext("/home/linner/IdeaProjects/my-springfw-demo/src/main/resources/applicationContext.xml");
      

      FileSystemXmlApplicationContext的参数为XML配置文件在文件系统下的绝对路径。

UserServiceTest执行成功说明配置成功。

Bean 的基础配置

<bean>标签是作为Spring配置Bean使用。其基本形式为:

<bean id="" class=""/>
  • id:Bean标签的唯一标识。在同一个上下文中(配置文件)不能重复。
  • class:Bean的类型。包含包名和类名。

别名(name 属性)

<bean>name属性可以为<bean>指定别名,别名可以有多个,使用,;,空格进行分隔。如:

<bean id="userService" name="service userEbi" class="com.linner.service.impl.UserServiceImpl"/>

说明:EBI全称Enterprise Business Interface,译为企业业务接口。

获取 Bean 的方式(getBean() 方法)

使用getBean()获取Bean的方式有三种:

  • 按照名称获取:

    是指在getBean()方法中传递String类型的参数,参数的值为Bean的名称。

    Object getBean(String s)
    
  • 按照类型获取:

    需要保证该类型在IOC容器中有且仅有一个Bean(不能包含多个同类型的Bean)。 在参数中传递该类型的class

    <T> T getBean(Class<T> aClass)
    
  • 按照名称和类型获取:

    在参数中传入Bean的名称和类型。

     <T> T getBean(String s, Class<T> aClass)
    

作用范围(scope 属性)

<bean>scope属性可以为<bean>设置作用范围,可选值为:

  • singloton:默认,表示创建的对象为单例(在整个IOC容器中所有获取到的该对象都为同一个对象)。如:

    <bean id="userService" class="com.linner.service.impl.UserServiceImpl" scope="singloton"/>
    

    或:

    <bean id="userService" class="com.linner.service.impl.UserServiceImpl" />
    

    实际上,单例对象在容器被获取的时候就已经被创建了。

  • prototype:表示创建的对象为非单例(每次在IOC容器中获取到的对象都是不同的对象)。如:

    <bean id="userService" name="service userEbi" class="com.linner.service.impl.UserServiceImpl" scope="prototype"/>
    

Example:

  • UserService设为单例:

    String配置文件applicationContext.xmluserService<bean>(由于Spring默认创建的是单例,所以这里可以选择不添加scope="singloton"):

    <bean id="userService" class="com.linner.service.impl.UserServiceImpl">
    </bean>
    

    创建新的测试类TestForUserServiceScope

    package com.linner.service;
    
    import com.linner.domain.User;
    import org.junit.Test;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    
    public class TestForUserServiceScope {
        private static ApplicationContext ctx;
        static {
            ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        }
    
        @Test
        public void testFindAll() {
            UserService userService = (UserService) ctx.getBean("userService");
            System.out.println(userService);
            userService.findAll();
            System.out.println("------------");
        }
    
        @Test
        public void testFindById() {
            UserService userService = (UserService) ctx.getBean("userService");
            System.out.println(userService);
            int id = 12;
            userService.findById(id);
            System.out.println("------------");
        }
    
        @Test
        public void testDelete() {
            UserService userService = (UserService) ctx.getBean("userService");
            System.out.println(userService);
            int id = 12;
            userService.delete(id);
            System.out.println("------------");
        }
    
        @Test
        public void testSave() {
            UserService userService = (UserService) ctx.getBean("userService");
            System.out.println(userService);
            User user = new User();
            user.setId(12);
            user.setUsername("abc");
            user.setPassword("abc123");
            userService.save(user);
            System.out.println("------------");
        }
    
        public void testUpdate() {
            UserService userService = (UserService) ctx.getBean("userService");
            System.out.println(userService);
            User user = new User();
            user.setId(12);
            user.setUsername("abc");
            user.setPassword("abc123");
            userService.update(user);
            System.out.println("------------");
        }
    }
    

    执行UserServiceTest1后,在输出中可以发现:每个测试方法中使用getBean()获取到的UserService都是同个对象。

  • UserService设为非单例:

    修改String配置文件applicationContext.xml

    <bean id="userService" class="com.linner.service.impl.UserServiceImpl" scope="prototype">
    

    再次执行UserServiceTest1,可以发现:每个测试方法中使用getBean()获取到的UserService都是不同的对象。

Bean 的实例化

实例化Bean分为三种方式:

  • 构造方法
  • 静态工厂
  • 实例工厂

构造方法实例化 Bean(默认)

<bean>默认是使用无参的构造方法实例化Bean。并且,Spring底层用的是反射(因为即使将构造方法设置为private依然可以使用)。

静态工厂实例化 Bean(factory-method 属性)

静态工厂实例化Bean指的是通过工厂类来创建对象。

一般情况下是使用如下方法静态工厂实例化对象:

  • 创建com.linner.factory包,并编写UserDaoFactory

    package com.linner.factory;
    
    import com.linner.dao.UserDao;
    import com.linner.dao.impl.UserDaoImpl;
    
    public class UserDaoFactory {
        public static UserDao getUserDao() {
            return new UserDaoImpl();
        }
    }
    
  • 编写TestFroInstanceUserDao测试类:

    package com.linner.dao;
    
    import com.linner.domain.User;
    import com.linner.factory.UserDaoFactory;
    import org.junit.Test;
    
    public class TestFroInstanceUserDao {
        private UserDao userDao = UserDaoFactory.getUserDao();
    
        @Test
        public void testFindAll() {
            userDao.findAll();
            System.out.println("------------");
        }
    
        @Test
        public void testFindById() {
            int id = 12;
            userDao.findById(id);
            System.out.println("------------");
        }
    
        @Test
        public void testDelete() {
            int id = 12;
            userDao.delete(id);
            System.out.println("------------");
        }
    
        @Test
        public void testSave() {
            User user = new User();
            user.setId(12);
            user.setUsername("abc");
            user.setPassword("abc123");
            userDao.save(user);
            System.out.println("------------");
        }
    
        @Test
        public void testUpdate() {
            User user = new User();
            user.setId(12);
            user.setUsername("abc");
            user.setPassword("abc123");
            userDao.update(user);
            System.out.println("------------");
        }
    }
    

在IOC容器中使用静态工厂实例化:

  • 在Spring配置文件中加入UserDao的配置:

    <bean id="userDao" class="com.linner.factory.UserDaoFactory" factory-method="getUserDao"/>
    
    • class:工厂类的类全名。这里指定的是创建UserDao的静态工厂类UserDaoFactory
    • factory-methodclass指定的工厂类中创建对象的方法名。这里指定的是工厂类中用来创建UserDao的方法。
  • 修改TestFroInstanceUserDao

    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    
    public class TestFroInstanceUserDao {
        private static UserDao userDao;
        static {
            ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
            userDao = (UserDao) ctx.getBean("userDao");
        }
        // ...
    }
    

注意:使用这种方法实例化,不能将该Bean注入到其他Bean中。

实例工厂实例化 Bean

使用 Bean 的 factory-bean 属性

实例化工厂实例化Bean是指,为工厂类添加一个单例的Bean(为区分则称为工厂Bean),然后在要实例化的Bean中指定使用这个工厂Bean作为实例化工厂。

修改UserDaoFactory(不使用静态工厂):

public class UserDaoFactory {
    public UserDao getUserDao() {
        return new UserDaoImpl();
    }
}

在Spring的配置文件中添加UserDaoFactory<bean>,并且修改UserDao<bean>

<bean id="userDaoFactory" class="com.linner.factory.UserDaoFactory"/>
<bean id="userDao" factory-bean="userDaoFactory" factory-method="getUserDao"/>
  • 添加了一个userDaoFactory工厂Bean。
  • factory-bean:工厂的实例对象,即工厂Bean的idname
  • factory-method:工厂对象中具体创建对象的方法名,即factory-bean指定对象中创建该对象的方法名。

成功执行TestFroInstanceUserDao即配置成功。

使用 FactoryBean 接口

创建一个UserDaoFactoryBean的类,实现FactoryBean接口,重写接口的方法:

package com.linner.factory;

import com.linner.dao.UserDao;
import com.linner.dao.impl.UserDaoImpl;
import org.springframework.beans.factory.FactoryBean;

public class UserDaoFactoryBean implements FactoryBean {

    @Override
    public Object getObject() throws Exception {
        return new UserDaoImpl();
    }

    @Override
    public Class<?> getObjectType() {
        return UserDao.class;
    }

    @Override
    public boolean isSingleton() {
        return true;
    }
}
  • getObject():创建实例化对象并返回。代替原始实例工厂中创建对象的方法。
  • getObjectType():返回所创建类的Class对象。
  • isSingleton():设置对象是否为单例。默认true,可以不重写。

修改Spring配置文件:

<bean id="userDao" class="com.linner.factory.UserDaoFactoryBean"/>

成功执行TestFroInstanceUserDao即配置成功。

Bean 的生命周期

Bean的生命周期是指Bean对象从创建到销毁的整个过程。对Bean的生命周期进行控制,可以在Bean创建后(如加载初始化需要用到资源)还有销毁前(如释放资源)执行一些操作。

init-method 和 destroy-method 属性

Spring控制生命周期的第一个方法便是使用Spring配置文件中<bean>init-methoddestroy-method 属性。

UserDaoImpl中创建初始化方法和销毁方法(方法名任意):

public class UserDaoImpl implements UserDao {

    /**
     * 初始化方法
     */
    public void init() {
        System.out.println("UserDao init...");
    }

    /**
     * 销毁方法
     */
    public void destroy() {
        System.out.println("UserDao destroy...");
    }

    // ...
}

修改Spring配置文件:

<bean id="userDao" class="com.linner.dao.impl.UserDaoImpl" init-method="init" destroy-method="destroy"/>
  • init-methodclass指定的类中的初始化方法,在创建Bean对象后执行。
  • destroy-methodclass指定的类中的销毁方法,在销毁Bean前执行。

编写TestForLifeCycle测试类:

package com.linner;

import com.linner.dao.UserDao;
import org.junit.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class TestForLifeCycle {
    @Test
    public void testForLifeCycle() {
        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        UserDao userDao = (UserDao) ctx.getBean("userDao");
        userDao.findAll();
        ctx.close();
    }
}

对比之前的ctx对象,这里ctx对象使用的类型从ApplicationContext变成了ClassPathXmlApplicationContext。这是因为ApplicationContext中并没有close()方法。而想要执行Bean对象中的destroy(),就必须在程序退出前关闭IOC容器(调用ctx.close())。

注册钩子关闭容器

在容器未关闭之前,提前设置好回调函数,让JVM在退出之前回调此函数来关闭容器。

修改TestForLifeCycle

public class TestForLifeCycle {
    @Test
    public void testForLifeCycle() {
        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        ctx.registerShutdownHook();
        UserDao userDao = (UserDao) ctx.getBean("userDao");
        userDao.findAll();
    }
}

注意:ApplicationContext中也没有registerShutdownHook()。所以这里使用的是ClassPathXmlApplicationC对象。

InitializingBean 和 DisposableBean 接口

Spring 提供了InitializingBeanDisposableBean接口以更加方便地控制Bean生命周期。使用这两个接口则无需通过Sping配置文件中的init-methoddestroy-method 属性来控制生命周期。

实现InitializingBean接口的afterPropertiesSet()方法和DisposableBean接口的destroy()方法:

import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;

public class UserDaoImpl implements UserDao, InitializingBean, DisposableBean {

    /**
     * InitializingBean接口的初始化方法
     */
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("UserDao init by afterPropertiesSet()...");
    }

    /**
     * DisposableBean接口的销毁方法
     */
    @Override
    public void destroy() {
        System.out.println("UserDao destroyed by destroy()...");
    }

    // ...
}

在Spring配置文件中删除init-methoddestroy-method属性:

<bean id="userDao" class="com.linner.dao.impl.UserDaoImpl"/>

使用 Bean 标签的注意事项

  • class属性不能使用接口。因为接口不能创建对象。

  • 获取Bean无论是通过id还是name获取,如果无法获取到,将抛出异常NoSuchBeanDefinitionException

  • Bean默认为单例,避免了对象的频繁创建与销毁,达到了对Bean对象的复用,性能高。

  • 如果对象是有状态对象(即该对象有成员变量可以用来存储数据)。因为所有请求线程共用一个Bean对象,所以会存在线程安全问题。

  • 如果对象是无状态对象(即该对象没有成员变量没有进行数据存储)。因方法中的局部变量在方法调用完成后会被销毁,所以不会存在线程安全问题。

  • 适合交给容器进行管理的Bean对象:

    • 表现层对象
    • 业务层对象
    • 数据层对象
    • 工具对象
  • 不适合交给容器进行管理的Bean对象:

    封装实例的域对象。会引发线程安全问题。

DI 依赖注入

上面的程序,在UserServiceImpl中依然需要手动创建对象:

public class UserServiceImpl implements UserService {
    private UserDao userDao = new UserDaoImpl();
    // ...
}

当前业务层和数据层的耦合性还是很高。此时就需要使用依赖注入来降低耦合性。

Spring依赖注入支持简单数据类型引用数据类型还有集合注入,并且提供了两种注入方式:

  • Setter注入
  • 构造器注入

基础配置:

  1. 创建Maven项目。
  2. 添加Spring Framework和Junit的依赖jar包。
  3. 添加业务层、数据层类:
  • 测试方法:

    package com.linner.service;
    
    import com.linner.domain.User;
    import com.linner.service.UserService;
    import com.linner.service.impl.UserServiceImpl;
    import org.junit.Test;
    
    import java.util.List;
    
    public class UserServiceTest {
        private static UserService userService;
        static {
            ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
            userService = (UserService) ctx.getBean("userService");
        }
    
        @Test
        public void testFindAll() {
            userService.findAll();
            System.out.println("------------");
        }
    
        @Test
        public void testFindById() {
            int id = 12;
            userService.findById(id);
            System.out.println("------------");
        }
    
        @Test
        public void testDelete() {
            int id = 12;
            userService.delete(id);
            System.out.println("------------");
        }
    
        @Test
        public void testSave() {
            User user = new User();
            user.setId(12);
            user.setUsername("abc");
            user.setPassword("abc123");
            userService.save(user);
            System.out.println("------------");
        }
    
        @Test
        public void testUpdate() {
            User user = new User();
            user.setId(12);
            user.setUsername("abc");
            user.setPassword("abc123");
            userService.update(user);
            System.out.println("------------");
        }
    }
    

Setter 注入(property 标签)

Setter注入是指,在实现类中为需要注入的属性设置Setter方法,让Spring能够使用Setter方法自动给属性创建对象。

注入引用数据类型

要在Bean中注入引用类型属性,注入的属性其实现类必须得是IOC容器中的Bean。

UserServiceImpl中,取消手动创建对象,并为userDao属性提供setter方法:

public class UserServiceImpl implements UserService {
    private UserDao userDao;

    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }
    // ...
}

修改Spring配置文件applicationContext.xml

<!--...-->
<bean id="userDao" class="com.linner.dao.impl.UserDaoImpl"/>
<bean id="userService" class="com.linner.service.impl.UserServiceImpl">
    <property name="userDao" ref="userDao"/>
</bean>
<!--...-->

UserService注入UserDao要在UserService<bean>标签中使用<property>标签。·

<property>表示配置当前<bean>的属性,其标签属性有:

  • name<bean>所指的实现类的属性名,表示配置哪一个具体的属性。
  • ref:要配置的属性的Bean的idname,表示参照哪一个<bean>(该注入的Bean必须在容器中存在)。

成功运行ServiceBeanTest说明注入成功。

如果要注入多个属性,则在实例类中加入多个属性,为其设置Setter并在Bean中配置多个<property>即可。

注入简单数据类型

在Bean中注入简单类型属性,简单数据类型并不用在配置文件中为简单类型添加Bean标签配置。

BookDao添加简单类型属性:

import org.springframework.beans.factory.InitializingBean;

public class UserDaoImpl implements UserDao, InitializingBean {

    private String databaseName;
    private int connectionNum;

    public void setConnectionNum(int connectionNum) {
        this.connectionNum = connectionNum;
    }

    public void setDatabaseName(String databaseName) {
        this.databaseName = databaseName;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("UserDao init: DatabaseName=" + this.databaseName
                        + ", ConnectionNum=" + this.connectionNum);
    }
    // ...
}

在Spring配置文件中为UserDao添加<property>标签:

<bean id="userDao" class="com.linner.dao.impl.UserDaoImpl">
    <property name="databaseName" value="mysql"/>
    <property name="connectionNum" value="10"/>
</bean>
  • name:含义不变,与引用注入类型用法中的含义相同。
  • value: 要配置的属性的值,必须是简单数据类型。对于参数类型,Spring在注入的时候会自动转换,但是有可能会发生转换错误的情况。

成功运行ServiceBeanTest说明注入成功。

集合注入

修改UserDao,为其添加集合类型参数和对应Setter方法:

import java.util.*;

public class UserDaoImpl implements UserDao, InitializingBean {

    private int[] array;
    private List<String> list;
    private Set<String> set;
    private Map<String,String> map;
    private Properties properties;

    // ...此处省略setter方法

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("UserDao init: " +
                        "array=" + Arrays.toString(array) +
                        ", list=" + list +
                        ", set=" + set +
                        ", map=" + map +
                        ", properties=" + properties);
    }
    // ...
}

修改Spring配置文件:

<bean id="userDao" class="com.linner.dao.impl.UserDaoImpl">
    <property name="array">
        <array>
            <value>1</value>
            <value>2</value>
            <value>3</value>
        </array>
    </property>
    <property name="list">
        <list>
            <value>a</value>
            <value>b</value>
            <value>c</value>
        </list>
    </property>
    <property name="set">
        <set>
            <value>a</value>
            <value>a</value>
            <value>b</value>
            <value>c</value>
        </set>
    </property>
    <property name="map">
        <map>
            <entry key="a" value="1"/>
            <entry key="b" value="2"/>
            <entry key="c" value="3"/>
        </map>
    </property>
    <property name="properties">
        <props>
            <prop key="a">1</prop>
            <prop key="b">2</prop>
            <prop key="c">3</prop>
        </props>
    </property>
</bean>
  • <property>:Setter注入依然使用该标签。其name属性含义不变。

    集合类型的值在其标签下定义。

  • <array>:定义数组类型的值。

    • 数组元素中的值使用<value>标签定义。
    • <value>标签中的值要与数组的类型一致。 一般形式如下:
    <property name="...">
        <array>
            <value>...</value>
            <value>...</value>
            <value>...</value>
            <!--...-->
        </array>
    </property>
    
  • <list>:定义List类型的值。

    其用法与<array>类似。

    <property name="...">
        <list>
            <value>...</value>
            <value>...</value>
            <value>...</value>
            <!--...-->
        </list>
    </property>
    
  • <set>:定义Set类型的值。 其用法与<array>类似。

    <property name="...">
        <set>
            <value>...</value>
            <value>...</value>
            <value>...</value>
            <!--...-->
        </set>
    </property>
    
  • <map>:定义Map类型的值。

    • 其元素使用<entry>标签定义。并且元素的key使用key属性定义,元素的value使用value属性定义。
    • keyvalue属性的值要和Map的类型对应。
    <property name="...">
        <map>
            <entry key="..." value="..."/>
            <entry key="..." value="..."/>
            <entry key="..." value="..."/>
            <!--...-->
        </map>
    </property>
    
  • <props>:定义Properties的值。

    其元素使用<prop>标签定义。key使用key属性定义,value在标签中定义。

    <property name="...">
        <props>
            <prop key="...">...</prop>
            <prop key="...">...</prop>
            <prop key="...">...</prop>
            <!--...-->
        </props>
    </property>
    

注意:

  • List的底层也是通过数组实现的,所以<list><array>标签是可以混用。
  • 集合中要添加引用类型,只需要把<value>标签改成<ref>标签。

构造器注入(constructor-arg 标签)

构造器注入是指Spring通过Bean的实例类中,带参的构造方法将其他Bean进行注入。

注入引用数据类型

UserServices中删除Setter方法(此处删除Setter方法是为了证明使用的是构造器注入而非Setter注入)并添加带参的构造方法:

public class UserServiceImpl implements UserService {

    private UserDao userDao;

    public UserServiceImpl(UserDao userDao) {
        this.userDao = userDao;
    }
    // ...
}

修改Spring配置文件:

<bean id="userService" class="com.linner.service.impl.UserServiceImpl">
    <constructor-arg name="userDao" ref="userDao"/>
</bean>

<constructor-arg>nameref属性的含义与<property>的含义类似。只不过name属性指的是Bean实例类型中构造方法的参数名(名称必须要一致)。

成功运行ServiceBeanTest说明注入成功。

如果要注入多个类型数据,则在构造方法中加入多个参数并在Bean中配置多个<constructor-arg>即可。

注入简单数据类型

UserDao中删除Setter方法,并为其添加带参的构造方法:

public class UserDaoImpl implements UserDao, InitializingBean {

    private String databaseName;
    private int connectionNum;

    public UserDaoImpl(String databaseName, int connectionNum) {
        this.databaseName = databaseName;
        this.connectionNum = connectionNum;
    }
    // ...
}

修改Spring配置文件:

<bean id="userDao" class="com.linner.dao.impl.UserDaoImpl">
    <constructor-arg name="databaseName" value="mysql"/>
    <constructor-arg name="connectionNum" value="10"/>
</bean>

成功运行ServiceBeanTest说明注入成功。

集合注入

构造器注入集合数据类型的方式与Setter注入集合数据类型的方式类似。

只不构造器注入要定义相应的带参方法,并且将<property>标签改为<constructor-arg>

自动装配(bean 标签的 autowire 属性)

自动装配只适用于引用类型依赖注入,不能对简单类型进行操作。

自动装配的方式有:

  • 按类型(常用)
  • 按名称
  • 按构造方法(的参数)

按类型自动装配

UserService中的属性(如userDao)提供Setter方法。接着在Spring中为UserService删除其<bean>标签下的所有<proerty><constructor-arg>。然后给<bean>添加autowire="byType"

<bean id="userService" class="com.linner.service.impl.UserServiceImpl" autowire="byType"/>

注意:

  • 类中对应属性的Setter方法不能省略。
  • 被注入的对象必须要被Spring的IOC容器管理。
  • 按照类型自动装配如果Spring在IOC中找到多个类型相同的Bean则会报NoUniqueBeanDefinitionException错误。

按名称自动装配

一个类型在IOC中有多个对象,还想要注入成功,这个时候就需要按照名称注入。

修改Spring配置文件(添加autowire="byName"):

<bean id="userService" class="com.linner.service.impl.UserServiceImpl" autowire="byName"/>

注意:

  • 按名称自动装配是根据Setter方法的名称在IOC容器中寻找名称对应的Bean(Bean的idname)。如setUserDao()则需要寻找名称为userDao的Bean。
  • 如果按照名称找不到对应的Bean,则注入null(不会报错!)。
  • 按名称自动装配中,实例类成员变量名与配置耦合,不推荐使用。

配置文件管理第三方 Bean(properties 文件加载)

数据源对象管理(DAO层)

以Druid为例:

  • pom.xml依赖:

    <dependencies>
        <!--Spring Framework-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.10.RELEASE</version>
        </dependency>
        <!--MySQL JDBC-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.29</version>
        </dependency>
        <!--Druid-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.11</version>
        </dependency>
    </dependencies>
    
  • JDBC配置(jdbc.properties):

    jdbc.driver=com.mysql.jdbc.Driver
    jdbc.url=jdbc:mysql://localhost:33061/spring_db
    jdbc.username=root
    jdbc.password=n546,Lin0
    
  • Spring配置(applicationContext.xml):

    <?xml version="1.0" encoding="UTF-8"?>
    <!--需要开启context命名空间-->
    <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:context="http://www.springframework.org/schema/context"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
    
        <!--加载jdbc配置文件-->
        <context:property-placeholder location="classpath*:jdbc.properties"/>
    
        <!--Druid Bean-->
        <bean class="com.alibaba.druid.pool.DruidDataSource">
            <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>
    
    </beans>
    
  • 编写App.java

    package com.linner;
    
    import com.alibaba.druid.pool.DruidDataSource;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    
    public class App {
        public static void main(String[] args) {
            ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
            DruidDataSource dataSource = ctx.getBean(DruidDataSource.class);
            System.out.println(dataSource);
        }
    }
    

    如果终端成功输出dataSource,说明配置成功。


注解开发

Spring对注解支持的版本历程:

  • 2.0版开始支持注解
  • 2.5版注解功能趋于完善
  • 3.0版支持纯注解开发

基础配置:

  1. 创建Maven项目。
  2. 添加Spring Framework和Junit的依赖jar包。
  3. 添加业务层、数据层类:

Spring 配置文件包扫描

为了能够获取到使用注解定义的Bean,需要在Spring配置文件中使用包扫描来识别出指定包下的所有Bean。

创建Spring配置文件。在Spring配置文件中加入包扫描:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

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

注意:

使用<context:component-scan>包扫描,需要在<beans>标签中加入属性:

xmlns:context="http://www.springframework.org/schema/context"

接着在<context:component-scan>xsi:schemaLocation属性中加入值:

http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd

<context:component-scan>

  • component:组件,Spring将管理的Bean视作自己的一个组件。

  • scan:扫描。

  • base-package:该属性指定Spring框架扫描的包路径,它会扫描指定包及其子包中的所有类上的注解。

    • 包路径越精确,如:com.linner.dao.impl,扫描的范围越小速度越快。
    • 包路径越广泛,如:com.linner,扫描的范围越大速度越慢。

    一般扫描到项目的组织名称即Maven的<groupId>下(如:com.linner)即可。 包扫描不仅能扫描当前包,还能扫描当前包下的子包。

创建 Bean 对象(@Component)

UserDao中添加@Component注解:

import org.springframework.stereotype.Component;

@Component("userDao")
public class UserDaoImpl implements UserDao {
    // ...
}

编写新的测试类UserDaoTest

package com.linner.dao;

import com.linner.domain.User;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class UserDaoTest {
    private static UserDao userDao;
    static {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        userDao = (UserDao) ctx.getBean("userDao");
    }

    @Test
    public void testFindAll() {
        userDao.findAll();
        System.out.println("------------");
    }

    @Test
    public void testFindById() {
        int id = 12;
        userDao.findById(id);
        System.out.println("------------");
    }

    @Test
    public void testDelete() {
        int id = 12;
        userDao.delete(id);
        System.out.println("------------");
    }

    @Test
    public void testSave() {
        User user = new User();
        user.setId(12);
        user.setUsername("abc");
        user.setPassword("abc123");
        userDao.save(user);
        System.out.println("------------");
    }

    @Test
    public void testUpdate() {
        User user = new User();
        user.setId(12);
        user.setUsername("abc");
        user.setPassword("abc123");
        userDao.update(user);
        System.out.println("------------");
    }
}
  • @Component可以传递一个String类型的值。用于制定Bean的名称。

  • @Component可以直接使用而不指定名称。即,使用按类型获取Bean。但必须保证该类型在IOC中有且仅有一个Bean。如:

    @Component("userDao")
    public class UserDaoImpl implements UserDao {
        // ...
    }
    
    UserDao userDao = ctx.getBean(UserDao.class);
    
  • @Component注解如果不起名称,会有一个默认值,就是将当前类名首字母转为小写后的值。所以也可以按照名称获取。

  • @Component还有其他三个衍生注解。

    • @Controller:表现层
    • @Service:业务层
    • @Repository:数据层

    它们的作用和@Component是一样的,仅作为对表现层、业务层和数据层的类进行区分。如:

    import org.springframework.stereotype.Repository;
    
    @Repository("userDao")
    public class UserDaoImpl implements UserDao {
        // ...
    }
    
    import org.springframework.stereotype.Repository;
    
    @Repository
    public class UserDaoImpl implements UserDao {
        // ...
    }
    

纯注解开发(配置类、包扫描)

Spring3.0开启了纯注解开发模式,使用Java类替代配置文件。

删除Spring配置文件并创建配置类:

package com.linner.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan("com.linner")
public class SpringConfig {
}
  • @Configuration:设定该类为Spring配置类。

  • @ComponentScan:包扫描,用来替换配置文件中的<context:component-scan>。在一个配置类中仅能使用一次。多个扫描路径使用数据格式,如:

    @ComponentScan({"com.linner.dao", "com.linner.service"})
    

    包扫描不仅能扫描当前包,还能扫描当前包下的子包

修改UserDaoTest

import com.linner.config.SpringConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class UserDaoTest {
    private static UserDao userDao;
    static {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        userDao = (UserDao) ctx.getBean("userDao");
    }
    // ...
}

使用Spring配置类,获取IOC容器应该使用AnnotationConfigApplicationContext对象:

public AnnotationConfigApplicationContext(Class<?>... componentClasses)

作用范围(@Scope)

注解开发控制Bean的作用范围使用@Scope。它的值有两个:

  • "singleton":默认值,将当前类设置为单例。

  • "prototype":将当前类设置为多例。如:

    import org.springframework.context.annotation.Scope;
    
    @Repository
    @Scope("prototype")
    public class UserDaoImpl implements UserDao {
        // ...
    }
    

生命周期控制(@PostConstruct 和 @PreDestroy)

注解控制Bean生命周期使用:

  • @PostConstruct:初始化。
  • @PreDestroy:销毁。

UserDao加入初始化和销毁方法(方法名任意):

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

@Repository
public class UserDaoImpl implements UserDao {

    /**
     * 初始化方法
     */
    @PostConstruct
    public void init() {
        System.out.println(this + "init...");
    }

    /**
     * 销毁方法
     */
    @PreDestroy
    public void destroy() {
        System.out.println(this + "destroy...");
    }
    // ...
}

与使用注解开发的情况一样,要想执行destroy()需要在程序执行的时候关闭容器:

ctx.close();

或:

ctx.registerShutdownHook();

注意:@PostConstruct和@PreDestroy注解如果找不到,需要导入下面的jar包。

<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>

原因是,从JDK9以后jdk中的javax.annotation包被移除了,这两个注解刚好就在这个包中。

依赖注入

UserService配置Bean:

import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements UserService {
    // ...
}

按照类型注入

注解开发按照类型注入使用@Autowired注解。

UserService注入UserDao

import org.springframework.beans.factory.annotation.Autowired;

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserDao userDao;
    
    // ...
}

@Autowired可以在实例类的成员变量上使用,也可以在Setter方法上使用,如:

@Service
public class UserServiceImpl implements UserService {

    private UserDao userDao;

    @Autowired
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }
    // ...
}

在实例类的成员变量上使用@Autowired,即使将Setter方法注释掉依然可以注入成功。因为自动装配基于反射设计创建对象并通过暴力反射为属性进行设值(即使是私有属性也能设值)。

使用@Autowired需要保证该类型在IOC容器中有且仅有一个同类型的Bean。 如果IOC容器中有对个同类型的Bean。那么@Autowired就会按照变量名和Bean的名称进行匹配。如:

@Repository("userDao")
public class UserDaoImpl implements UserDao { /*...*/ }
@Service
public class UserServiceImpl implements UserService {
    @Autowired
    private UserDao userDao;
    // ...
}

也能注入成功。

如果有多个同类型Bean,并且@Autowired名称匹配不上,那么会报NoUniqueBeanDefinitionException错误。

使用使用IOC容器创建Bean中的UserServiceTest,成功执行说明配置成功。

按照名称注入

注解开发名称注入使用@Qualifier注解来完成。

import org.springframework.beans.factory.annotation.Qualifier;

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    @Qualifier("userDao")
    private UserDao userDao;
    
    // ...
}

@Qualifier中指定Bean的名称。并且,@Qualifier必须和@Autowired一起使用才能起到按照名称注入的效果。

经过测试单独使用@Qualifier(不加@Autowired)的效果和按照类型注入的效果一样(可能会有其他细微的区别)。

注入简单数据类型

注解开发注入简单类型数据使用@Value注解。

import org.springframework.beans.factory.annotation.Value;

@Repository
public class UserDaoImpl implements UserDao {

    @Value("mysql")
    private String databaseName;
    @Value("10")
    private int connectionNum;

    // ...
}

@Value中的值使用String类型。如果要注入的是其他类型数据,需要按照其类型数据的格式编写。

注解读取 properties 配置文件

使用@Value的意义之一在于:读取properties配置文件的信息。

resource目录下新建jdbc.properties

databaseName=mysql
connectionNum=10

在配置类上添加@PropertySource注解:

import org.springframework.context.annotation.PropertySource;

@Configuration
@ComponentScan("com.linner")
@PropertySource("jdbc.properties")
public class SpringConfig {
}

修改UserDao

@Repository
public class UserDaoImpl implements UserDao {

    @Value("${databaseName}")
    private String databaseName;
    @Value("${connectionNum}")
    private int connectionNum;
    
    // ...
}
  • 读取多个配置文件,使用数组形式给@PropertySource传递字符串数组。如:

    @PropertySource({"jdbc.properties", "xxx.properties"})
    
  • @PropertySource注解属性中不支持使用通配符*,运行会报错。

  • @PropertySource注解属性中可以加入classpath:,代表从当前项目的根路径找文件:

    @PropertySource({"classpath:jdbc.properties"})
    

注解管理第三方 Bean

基础配置:

  • User.java: 同使用IOC容器创建Bean中的User.java

  • UserDao

    package com.linner.dao;
    
    import com.linner.domain.User;
    import org.apache.ibatis.annotations.*;
    
    import java.util.List;
    
    public interface UserDao {
    
        @Select("SELECT * FROM user_tb")
        List<User> findAll();
    
        @Select(("SELECT * FROM user_tb WHERE id = #{id}"))
        User findById(int id);
    
        @Delete("DELETE FROM user_tb WHERE id = #{id}")
        void delete(int id);
    
        @Insert("INSERT INTO user_tb(username, password) VALUES (#{username}, #{password})")
        void save(User user);
    
        @Update("UPDATE user_tb SET username = #{username}, password = #{password} WHERE id = #{id}")
        void update(User user);
    }
    
  • UserService

    UserService接口同使用IOC容器创建Bean中的UserService.java

    UserService实现类UserServiceImpl.java

    package com.linner.service.impl;
    
    import com.linner.dao.UserDao;
    import com.linner.domain.User;
    import com.linner.service.UserService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    import java.util.List;
    
    @Service
    public class UserServiceImpl implements UserService {
    
        @Autowired
        private UserDao userDao;
    
        @Override
        public List<User> findAll() {
            return userDao.findAll();
        }
    
        @Override
        public User findById(int id) {
            return userDao.findById(id);
        }
    
        @Override
        public void delete(int id) {
            userDao.delete(id);
        }
    
        @Override
        public void save(User user) {
            userDao.save(user);
        }
    
        @Override
        public void update(User user) {
            userDao.update(user);
        }
    }
    

整合 Mybatis

  • pom.xml依赖:

    <!--Spring Framework-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.2.10.RELEASE</version>
    </dependency>
    <!--MySQL JDBC-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.29</version>
    </dependency>
    <!--Druid-->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.2.11</version>
    </dependency>
    <!--MyBatis-->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.10</version>
    </dependency>
    <!--Spring JDBC-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>5.2.10.RELEASE</version>
    </dependency>
    <!--MyBatis Spring 整合包-->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis-spring</artifactId>
        <version>1.3.0</version>
    </dependency>
    <!--Junit 单元测试-->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.13</version>
        <scope>test</scope>
    </dependency>
    
  • 建立User表:

    DROP TABLE IF EXISTS `user_tb`;
    CREATE TABLE `user_tb`  (
    `id` int(10) PRIMARY KEY AUTO_INCREMENT COMMENT '用户id',
    `username` varchar(10) NOT NULL COMMENT '用户名',
    `password` varchar(10) NOT NULL COMMENT '密码'
    ) CHARACTER SET = utf8;
    
  • JDBC配置文件jdbc.properties

    jdbc.driver=com.mysql.cj.jdbc.Driver
    jdbc.url=jdbc:mysql://localhost:3306/spring_db?useSSL=false&useServerPrepStmts=true
    jdbc.username=root
    jdbc.password=root
    
  • JDBC配置类JdbcConfig.java

    package com.linner.config;
    
    import com.alibaba.druid.pool.DruidDataSource;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.context.annotation.Bean;
    
    import javax.sql.DataSource;
    
    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;
    
        /**
         * 获取DataSource对象
         */
        @Bean
        public DataSource dataSource() {
            DruidDataSource ds = new DruidDataSource();
            // 设置驱动信息
            ds.setDriverClassName(this.driver);
            ds.setUrl(this.url);
            ds.setUsername(this.username);
            ds.setPassword(this.password);
            return ds;
        }
    }
    
    • @Bean:用于方法上,根据返回值类型自动产生Bean,并且方法的参数会按照类型自动装配。
  • Mybatis配置类MybatisConfig.java

    package com.linner.config;
    
    import org.mybatis.spring.SqlSessionFactoryBean;
    import org.mybatis.spring.mapper.MapperScannerConfigurer;
    import org.springframework.context.annotation.Bean;
    
    import javax.sql.DataSource;
    
    public class MybatisConfig {
    
        @Bean
        public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource) {
            SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
            // 设置模型类的别名扫描
            ssfb.setTypeAliasesPackage("com.linner.domain");
            // 设置数据源
            ssfb.setDataSource(dataSource);
            return ssfb;
        }
    
        /**
        * 获取MapperScannerConfigurer对象
        */
        @Bean
        public MapperScannerConfigurer mapperScannerConfigurer() {
            MapperScannerConfigurer msc = new MapperScannerConfigurer();
            msc.setBasePackage("com.linner.dao");
            return msc;
        }
    }
    
    • SqlSessionFactoryBean:用于封装SqlSessionFactory需要的环境信息(原先的信息是在Mybatis配置文件中定义,如今可以在Java中用代码定义)。用于产生SqlSessionFactory对象。
      • setTypeAliasesPackage():设置模型类的别名扫描
      • setDataSource():设置DataSource。这里设置的DataSource通过方法的参数和@Bean注解实现了自动装配。并且自动装配的对像是Druid的DataSource
    • MapperScannerConfigurer:用于加载Dao接口,创建代理对象保存到IOC容器中。此处设置的是dao包下的所有接口。这些接口都会被Mybatis创建成对象并且作为Bean保存在IOC容器中。即,使用ctx.getBean(UserDao.class)能获取到UserDao
  • Spring配置类SpringConfig.java

    package com.linner.config;
    
    import org.springframework.context.annotation.*;
    
    @Configuration
    @ComponentScan("com.linner")
    @PropertySource("classpath:jdbc.properties")
    @Import({JdbcConfig.class, MybatisConfig.class})
    public class SpringConfig {
    }
    
    • @Import:用于引入其他配置类。如果要引入多个配置类则需要使用数组形式。
  • 测试类UserServiceTest

    package com.linner;
    
    import com.linner.config.SpringConfig;
    import com.linner.domain.User;
    import com.linner.service.UserService;
    import org.junit.Test;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.annotation.AnnotationConfigApplicationContext;
    
    import java.util.List;
    
    public class UserServiceTest {
        private static UserService userService;
        static {
            ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
            userService = ctx.getBean(UserService.class);
        }
    
        @Test
        public void testFindAll() {
            List<User> users = userService.findAll();
            for (User user : users) {
                System.out.println(user);
            }
        }
    
        @Test
        public void testFindById() {
            int id = 10;
            User user = userService.findById(id);
            System.out.println(user);
        }
    
        @Test
        public void testDelete() {
            int id = 10;
            userService.delete(id);
        }
    
        @Test
        public void testSave() {
            User user = new User();
            user.setUsername("张三");
            user.setPassword("abc");
            userService.save(user);
        }
    
        @Test
        public void testUpdate() {
            User user = new User();
            user.setId(21);
            user.setUsername("张三");
            user.setPassword("abc");
            userService.update(user);
        }
    }
    

整合 Junit

  • 引入Spring与测试有关的整合包:

    <!--Spring 测试整合包-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>5.2.10.RELEASE</version>
    </dependency>
    
  • 修改测试类UserServiceTest

    import org.junit.runner.RunWith;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
    
    import java.util.List;
    
    // 设置类运行器
    @RunWith(SpringJUnit4ClassRunner.class)
    // 设置Spring环境对应的配置
    // 加载配置类
    @ContextConfiguration(classes = {SpringConfig.class})
    // 加载配置文件
    //@ContextConfiguration(locations={"classpath:applicationContext.xml"})
    public class UserServiceTest {
    
        // 自动装配UserService,删除static静态代码块
        @Autowired
        private UserService userService;
    
    
    }
    

AOP

AOP 介绍

一般常用的编程思想是OOP(面向对象编程,Object Oriented Programming)。

而AOP(面向切面编程,Aspect Oriented Programming),是一种编程范式,指导开发者如何组织程序结构。

编程思想主要的内容就是指导程序员该如何编写程序,所以AOP和OOP是两种不同的编程范式。

AOP的作用是在不改变原始设计的基础上为其进行功能增强。类似于Filter代理模式。

AOP术语:

  • 连接点(JoinPoint):程序执行过程中的任意位置,粒度为执行方法、抛出异常、设置变量等。

    在SpringAOP中,理解为方法的执行。

    例如要对项目中UserDao的功能在不进行任何修改的前提下实现增强。那么UserDao中要增强的方法即为连接点

  • 切入点(Pointcut):匹配连接点的式子。

    在SpringAOP中,一个切入点可以描述一个具体方法,也可也匹配多个方法。

    连接点范围要比切入点范围大,是切入点的方法也一定是连接点,但是是连接点的方法就不一定要被增强,所以可能不是切入点。

  • 通知(Advice):在切入点处执行的操作,也就是共性功能。

    在SpringAOP中,功能最终以方法的形式呈现。

  • 通知类:定义通知的类。

  • 切面(Aspect):描述通知与切入点的对应关系。

    通知是要增强的内容,会有多个;切入点是需要被增强的方法,也会有多个。通知和切入点的关系通过切面来描述。

  • 目标对象(Target):也叫原始对象。原始功能去掉共性功能对应的类产生的对象。即,配置AOP之前原设计的对象(要增强的对象)。

  • 代理(Proxy):通过通知类把目标对象增强后产生的对象。

    目标对象无法直接完成工作,需要对其进行功能回填,通过原始对象的代理对象实现。

注解开发 AOP

基础配置:使用整合 Junit中的配置。

  • pom.xml导入坐标:

    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.9.4</version>
    </dependency>
    
    • spring-context中已经包含了spring-aop,所以不需要再单独导入spring-aop
    • AspectJ是AOP思想的一个具体实现,AspectJ比起Spring的AOP实现来说,更加好用。
  • 创建通知类UserDaoAdvice

    package com.linner.aop;
    
    import org.aspectj.lang.annotation.*;
    import org.springframework.stereotype.Component;
    
    @Component
    @Aspect
    public class UserDaoAdvice {
    
        @Pointcut("execution(* com.linner.dao.UserDao.*(..))")
        private void pt() {}
    
        @Before("pt()")
        public void method() {
            System.out.println("Before...");
        }
    
    }
    
    • @Aspect:设置当前类为AOP切面类。

    • @Pointcut:设置切入点方法。切入点定义需要一个不具有实际意义的方法(无参、无返回值、空方法体)。即UserDaoAdvice中的pt()

      @Pointcut中的execution()用来定义切入点。

    • @Before:设置切面。即设置当前通知方法在原始切入点方法前运行。

      切入点定义作为@Before的参数。

  • SpringConfig加入@EnableAspectJAutoProxy注解:

    import org.springframework.context.annotation.*;
    
    @Configuration
    @ComponentScan("com.linner")
    @EnableAspectJAutoProxy
    @PropertySource("classpath:jdbc.properties")
    @Import({JdbcConfig.class, MybatisConfig.class})
    public class SpringConfig {
    }
    

AOP 切入点表达式

切入点表达式是对要进行增强的方法的描述方式。切入点表达式的一般形式如下:

动作关键字(访问修饰符 返回值类型 包名.类名.方法名(参数类型列表) 异常名)
  • 由于通常都是对public方法进行定义,而public是默认的,所以可以省略。
  • 切入点表达式定义的类可以是接口,也可以是其实现类。
  • AOP是在Spring中运行的,很显然切入点表达式定义的类必须存在IOC容器中。
  • 如果匹配无参方法,直接省略参数。

Example:

execution(void com.linner.dao.UserDao.delete(int))

切入点表达式通配符

  • *:匹配任意单个独立的任意符号。

    可以独立出现,匹配任意返回值类型、单个包名(不能用做匹配完整包名)、类名、方法名或单个参数类型(即匹配单个单词); 也可以作为前缀或者后缀的匹配符出现。

    如:

    execution* com.linner.*.UserService.find*(*))
    

    匹配com.linner包下的任意包中的UserService类或接口中,所有以find开头的、带有一个参数的、任意返回值类型的方法。

  • ..:匹配多个连续的任意符号。

    可以独立出现,常用于简化包名与参数的书写。

    execution* com..UserService.findById(..))
    

    匹配com包下的任意包中的UserService类或接口中所有名称为findById的(参数的数量、类型任意,返回值任意)方法。

  • +:专用于匹配子类类型。

    execution(* *..*Service+.*(..))
    

    这个使用率较低。*Service+,表示所有以Service结尾的接口的子类。

书写技巧(所有代码按照标准规范开发,否则以下技巧全部失效):

  • 描述切入点通常描述接口,而不描述实现类,如果描述到实现类,就出现耦合了。
  • 访问控制修饰符针对接口开发均采用public描述(简化书写)。
  • 返回值类型对于增删改方法使用精准类型加速匹配,对于查询类使用*通配快速描述。
  • 包名书写尽量不使用..匹配,效率过低,常用*做单个包描述匹配,或精准匹配。
  • 接口名/类名书写名称与模块相关的采用*匹配,例如UserService书写成*Service,绑定业务层接口名。
  • 方法名书写以动词进行精准匹配,名词采用*匹配,例如getById书写成getBy*selectAll书写成selectAll
  • 参数规则较为复杂,根据业务方法灵活调整。
  • 通常不使用异常作为匹配规则。

通知类型

AOP通知描述了抽取的共性功能,根据共性功能抽取的位置不同,最终运行代码时要将其加入到合理的位置。

AspactJ提供了5种通知类型:

  • 前置通知
  • 后置通知
  • 环绕通知
  • 返回后通知
  • 抛出异常后通知
// 代码1
try {
    // 代码2
    // 原始的业务操作
    // 代码3
} catch (Exception e) {
    // 代码4
}
// 代码5

其中:

  • 前置通知:代码1和代码2
  • 返回后通知:代码3
  • 抛出异常后通知:代码4
  • 后置通知:代码5
  • 环绕通知:上述整个代码块

通知方法的方法名称没有限制,只需在方法的上端使用注解定义即可。各通知类型的注解如下:

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

这些注解的使用方式都是一样的。在对应的方法上定义,并且传入切入点(函数形式)。如:@Around("pt()")

前置通知和后置通知的使用

前置通知和后置通知的使用方式一样:

@Component
@Aspect
public class UserDaoAdvice {
    @Pointcut("execution(* com.linner.dao.UserDao.*(..))")
    private void pt() {}

    @Before("pt()")
    public void before() {
        System.out.println("Before...");
    }

    @After("pt()")
    public void after() {
        System.out.println("After...");
    }
}
环绕通知的使用
import org.aspectj.lang.ProceedingJoinPoint;

@Component
@Aspect
public class UserDaoAdvice {
    @Pointcut("execution(* com.linner.dao.UserDao.findById(int))")
    private void pt() {}

    @Around("pt()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("Before...");
        Object ret = pjp.proceed();
        System.out.println("After....");
        return ret;
    }
}

环绕通知必须传入一个ProceedingJoinPoint参数。使用pjp.proceed()才可以在环绕通知中掉用原始方法。并且proceed()可以获取到原始方法的返回值。

如果也想让代理后的方法返回跟原始方法一样的值。那么必须定义环绕方法的返回值类型,并且使用proceed()获取原始方法的返回值并返回。

环绕通知比较灵活,可以将其定义成其他类型的通知,并且可以做到其他通知做不到的事情。例如循环调用原始方法等。

环绕通知需要抛出异常是因为。原始方法有可能会出现异常,并且原始方法的异常并不确定。所以为了提高通用性,环绕通知默认抛出异常。

如果环绕方法定义的是void类型,并且原始方法有返回值。如果此时在环绕通知中没有返回值,那么在掉用代理后的方法后,返回的是null

返回后通知的使用
@Component
@Aspect
public class UserDaoAdvice {
    @Pointcut("execution(* com.linner.dao.UserDao.findById(int))")
    private void pt() {}

    @AfterReturning("pt()")
    public void afterReturning() {
        System.out.println("After Returning...");
    }
}

注意:返回后通知是需要在原始方法正常执行后才会被执行,如果原始方法执行的过程中出现了异常,那么返回后通知不会被执行。

后置通知是不管原始方法有没有抛出异常都会被执行。

异常后通知
@Component
@Aspect
public class UserDaoAdvice {
    @Pointcut("execution(* com.linner.service.UserService.findAll())")
    private void pt() {}

    @AfterThrowing("pt()")
    public void afterThrowing() {
        System.out.println("After Throwing...");
    }
}

UserServiceImpl.findAll()中模拟错误即可触发。例如使用int a = 1/0;模拟错误。

注意:异常后通知是需要原始方法抛出异常。如果没有抛异常,异常后通知将不会被执行。