# ssm - spring

# 1. 核心概念

(1) 什么IOC/DI思想?

IOC : 控制反转,控制反转的是对象的创建权

DI : 依赖注入,绑定对象与对象之间的依赖关系

(2) 什么是 IOC 容器?

Spring 创建了一个容器用来存放所创建的对象,这个容器就叫 IOC 容器

(3) 什么是 Bean?

容器中所存放的一个个对象就叫 Bean 或 Bean对象

# 2. 入门案例

目录:

${root}
  src/main
      java/
          org/example
              App.java
              BookDao.java
              BookDaoImpl.java
              BookService.java
              BookServiceImpl.java
      resources/
          applicationContext.xml
          
  pom.xml

pom.xml:

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.2.10.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <scope>test</scope>
    </dependency>
</dependencies>

resources/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
        http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="bookDao" class="org.example.BookDaoImpl"/>

    <bean id="bookService" class="org.example.BookServiceImpl">
        <property name="bookDao" ref="bookDao"/>
    </bean>

</beans>

class:

public class App {
    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");

        BookService bookService = (BookService) ctx.getBean("bookService");
        bookService.save();
    }
}

public interface BookDao {
    public void save();
}
public class BookDaoImpl implements BookDao {
    public void save() {
      System.out.println("book dao save ...");
    }
}

public interface BookService {
    public void save();
}
public class BookServiceImpl implements BookService {
    private BookDao bookDao;

    public void setBookDao(BookDao bookDao) {
        this.bookDao = bookDao;
    }

    public void save() {
        System.out.println("book service save ...");
        bookDao.save();
    }

}

# 3. IOC 相关

# 3.1. bean 基础配置

<!-- 
  id: 必须唯一
  class: 指定实现类的全名,不可以写接口
  name: 指定别名,可定义多个,分隔符: 空格、分号、空格
  scope: "singleton" (单例,默认) | "prototype" (非单例)
 -->
<bean 
  id="bookDao"
  class="org.example.quickstart.BookDaoImpl"

  name="bookDao1, bookDao2"

  scope="singleton"
/>

# 3.2. bean 实例化

(1) 方式一: (无参)构造函数 实例化 bean

类:

// 实现类
public class BookDaoImpl implements BookDao {
    public void save() {
        System.out.println("book dao save ...");
    }
}

配置:

<bean id="bookDao" class="org.example.BookDaoImpl"/>

使用:

BookDao bookDao = (BookDao) ctx.getBean("bookDao");
bookDao.save();

(2) 方式二: 静态工厂 实例化 bean

类:

// 静态工厂类
public class OrderDaoFactory {
    public static OrderDao getOrderDao(){
        return new OrderDaoImpl();
    }
}

// 实现类
public class OrderDaoImpl implements OrderDao {
    public void save() {
        System.out.println("order dao save ...");
    }
}

配置:

<bean id="orderDao" class="org.example.OrderDaoFactory" factory-method="getOrderDao" />

使用:

OrderDao orderDao = (OrderDao) ctx.getBean("orderDao");
orderDao.save();

(3) 方式三: 实例工厂 实例化 bean

类:

// 实例工厂类
public class UserDaoFactory {
    public UserDao getUserDao(){
        return new UserDaoImpl();
    }
}

// 实现类
public class UserDaoImpl implements UserDao {
    public void save() {
        System.out.println("user dao save ...");
    }
}

配置:

<bean id="userFactory" class="org.example.UserDaoFactory"/>
<bean id="userDao" factory-method="getUserDao" factory-bean="userFactory"/>

使用:

UserDao userDao = (UserDao) ctx.getBean("userDao");
userDao.save();

(4) 方式四: FactoryBean 实例化 bean

类:

// 工厂类
public class UserDaoFactoryBean implements FactoryBean<UserDao> {
    @Override
    public UserDao getObject() throws Exception {
        return new UserDaoImpl(); // 返回 bean
    }

    @Override
    public Class<?> getObjectType() {
        return UserDao.class; // 指定 bean 的类型
    }
}

配置:

<bean id="userDao2" class="org.example.UserDaoFactoryBean" />

使用:

UserDao userDao = (UserDao) ctx.getBean("userDao2");
userDao.save();

# 3.3. bean 的生命周期

(1) 关于 Spring 中对 bean 生命周期控制提供了两种方式:

  • 在配置文件中的 bean 标签中添加 init-methoddestroy-method 属性
  • 类实现 InitializingBeanDisposableBean 接口
<bean id="bookDao" class="org.example.BookDaoImpl" init-method="init" destroy-method="destroy" />

<bean id="userDao" class="org.example.UserDaoImpl"/>
public class App {
    public static void main(String[] args) {
        ConfigurableApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");

        BookDaoImpl bookDao = (BookDaoImpl) ctx.getBean("bookDao");
        bookDao.save();
        UserDaoImpl userDao = (UserDaoImpl) ctx.getBean("userDao");
        userDao.save();

        // ctx.close();
        ctx.registerShutdownHook();
    }
}

public class BookDaoImpl {
    public void save() {
        System.out.println("book dao save ...");
    }

    public void init() {
        System.out.println("book dao init ...");
    }
    public void destroy() {
        System.out.println("book dao destroy ...");
    }
}

public class UserDaoImpl implements InitializingBean, DisposableBean {
    public void save() {
        System.out.println("user dao save ...");
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("user dao init ...");
    }
    @Override
    public void destroy() throws Exception {
        System.out.println("user dao destroy ...");
    }

}

(2) 对于 bean 的生命周期控制在 bean 的整个生命周期中所处的位置如下:

  1. 初始化容器:
    1. 创建对象(内存分配)
    2. 执行构造方法
    3. 执行属性注入(set操作)
    4. 执行bean初始化方法
  2. 使用 bean
    • 执行业务操作
  3. 关闭/销毁容器
    • 执行 bean 销毁方法

(3) 关闭容器的两种方式:

  • ConfigurableApplicationContext 是 ApplicationContext 的子类
  • close() 方法
  • registerShutdownHook() 方法

# 4. DI 相关内容

# 4.1. setter 注入

<bean id="bookDao" class="org.example.BookDaoImpl" />

<bean id="bookService" class="org.example.BookServiceImpl">
    <!-- 注入 引用类型 -->
    <property name="bookDao" ref="bookDao" />

    <!-- 注入 简单类型 -->
    <property name="name" value="ZhangSan" />
    <property name="num" value="18" />
</bean>
public class BookServiceImpl {
    private BookDaoImpl bookDao;
    private String name;
    private int num;

    public void setBookDao(BookDaoImpl bookDao) {
        this.bookDao = bookDao;
    }
    public void setName(String name) {
        this.name = name;
    }
    public void setNum(int num) {
        this.num = num;
    }

    public void save() {
        bookDao.save();
        System.out.println("book service save ... name=" + name + ", num=" + num);
    }
}

# 4.2. 构造器注入

<bean id="bookDao" class="org.example.BookDaoImpl" />

<bean id="bookService" class="org.example.BookServiceImpl">
    <!-- name 为形参的名称 -->
    <constructor-arg name="bookDao" ref="bookDao" />
    <constructor-arg name="name" value="ZhangSan" />
    <constructor-arg name="num" value="20" />
</bean>

public class BookServiceImpl {
    private BookDaoImpl bookDao;
    private String name;
    private int num;

    public BookServiceImpl(BookDaoImpl bookDao, String name, int num) {
        this.bookDao = bookDao;
        this.name = name;
        this.num = num;
    }

    public void save() {
        bookDao.save();
        System.out.println("book service save ... name=" + name + ", num=" + num);
    }
}

介绍完两种参数的注入方式,具体我们该如何选择呢?

  1. 强制依赖使用构造器进行,使用 setter 注入有概率不进行注入导致 null 对象出现
    • 强制依赖指对象在创建的过程中必须要注入指定的参数
  2. 可选依赖使用 setter 注入进行,灵活性强
    • 可选依赖指对象在创建过程中注入的参数可有可无
  3. Spring 框架倡导使用构造器,第三方框架内部大多数采用构造器注入的形式进行数据初始化,相对严谨
  4. 如果有必要可以两者同时使用,使用构造器注入完成强制依赖的注入,使用 setter 注入完成可选依赖的注入
  5. 实际开发过程中还要根据实际情况分析,如果受控对象没有提供 setter 方法就必须使用构造器注入
  6. 自己开发的模块推荐使用 setter 注入

# 4.3. 自动装配

说明:

  • IoC 容器根据 bean 所依赖的资源在容器中自动查找并注入到 bean 中的过程称为自动装配
  • 针对引用类型自动装配

自动装配方式:

  • 按类型(常用)
  • 按名称

示例:

<bean class="org.example.BookDaoImpl" />

<bean id="bookService" class="org.example.BookServiceImpl" autowire="byType" />
public class BookServiceImpl implements BookService {
    private BookDao bookDao;

    public void setBookDao(BookDao bookDao) {
        this.bookDao = bookDao;
    }

    @Override
    public void save() {
        System.out.println("book service save ...");
        bookDao.save();
    }
}

注意事项:

  • 需要注入属性的类中对应属性的 setter 方法不能省略
  • 被注入的对象必须要被 Spring 的 IO 容器管理
  • 按照类型在 Spring 的 IOC 容器中如果找到多个对象,会报 NoUniqueBeanDefinitionException

# 4.4. 集合注入

集合类型:

  • Array
  • List
  • Set
  • Map
  • Properties

示例:

public class BookDaoImpl implements BookDao {
    private int[] array;
    private List<String> list;
    private Set<String> set;
    private Map<String,String> map;
    private Properties properties;

    public void setArray(int[] array) {
        this.array = array;
    }
    public void setList(List<String> list) {
        this.list = list;
    }
    public void setSet(Set<String> set) {
        this.set = set;
    }
    public void setMap(Map<String, String> map) {
        this.map = map;
    }
    public void setProperties(Properties properties) {
        this.properties = properties;
    }

    public void save() {
      System.out.println("book dao save ...");

      System.out.println("遍历数组:" + Arrays.toString(array));
      System.out.println("遍历List" + list);
      System.out.println("遍历Set" + set);
      System.out.println("遍历Map" + map);
      System.out.println("遍历Properties" + properties);
    }
}
<bean id="bookDao" class="org.example.BookDaoImpl">
    <property name="array">
        <array>
            <value>1</value>
            <value>2</value>
        </array>
    </property>
    <property name="list">
        <list>
            <value>ZhangSan</value>
            <value>LiSi</value>
        </list>
    </property>
    <property name="set">
        <set>
            <value>数学</value>
            <value>语文</value>
        </set>
    </property>
    <property name="map">
        <map>
            <entry key="code" value="0"/>
            <entry key="message" value="success"/>
        </map>
    </property>
    <property name="properties">
        <props>
            <prop key="name">张三</prop>
            <prop key="age">18</prop>
        </props>
    </property>
</bean>

# 5. IOC/DI 配置管理第三方 bean

# 5.1. 案例:数据源对象管理

Druid(德鲁伊) / C3P0

<!--  
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.16</version>
        </dependency>
-->
<bean class="com.alibaba.druid.pool.DruidDataSource">
    <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql://localhost:3306/spring_db"/>
    <property name="username" value="root"/>
    <property name="password" value="123456"/>
</bean>

<!--  
        <dependency>
            <groupId>c3p0</groupId>
            <artifactId>c3p0</artifactId>
            <version>0.9.1.2</version>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <version>8.3.0</version>
        </dependency>
-->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
    <property name="driverClass" value="com.mysql.jdbc.Driver"/>
    <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/spring_db"/>
    <property name="user" value="root"/>
    <property name="password" value="123456"/>
    <property name="maxPoolSize" value="10"/>
</bean>
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
DataSource dataSource = (DataSource) ctx.getBean("dataSource");
System.out.println(dataSource);

# 5.2. 加载 properties 文件

<!-- 
    开 context 名称空间
-->
<?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:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

    <!--
        system-properties-mode="NEVER"
            不加载系统环境变量,避免与文件中定义的变量冲突

        location="classpath:jdbc.properties"
            加载当前项目类路径下的 jdbc.properties
        location="classpath:1.properties, 2.properties"
            加载当前项目类路径下的 多个文件
        location="classpath:*.properties"
            加载当前项目类路径下的所有 .properties 文件
        location="classpath*:*.properties"
            加载 当前项目类路径及jar包 中所有 .properties 文件
    -->
    <context:property-placeholder location="classpath:jdbc.properties" system-properties-mode="NEVER"/>
    <bean id="dataSource" class="org.example.MyDataSource">
        <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>

jdbc.properties:

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://127.0.0.1:3306/spring_db
jdbc.username=root
jdbc.password=123456
public class MyDataSource {
    private String driverClassName;
    private String url;
    private String username;
    private String password;

    public void setDriverClassName(String driverClassName) {
        this.driverClassName = driverClassName;
    }
    public void setUrl(String url) {
        this.url = url;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    public void setPassword(String password) {
        this.password = password;
    }

    public String toString() {
        return "driverClassName=" + driverClassName
                + ", url=" + url
                + ", username=" + username
                + ", password=" + password
        ;
    }
}

# 6. 核心容器

容器的创建方式:

// 类路径下的XML配置文件
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");

// 文件系统下的XML配置文件
ApplicationContext ctx = new FileSystemXmlApplicationContext("D:\\applicationContext.xml");

Bean的三种获取方式:

BookDao bookDao = (BookDao) ctx.getBean("bookDao");

BookDao bookDao = ctx.getBean("bookDao"BookDao.class);

BookDao bookDao = ctx.getBean(BookDao.class);

容器类层次结构:

  • BeanFactory: 最上级的父接口

# 7. IOC/DI 注解开发

# 7.1. 注解开发定义 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"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

    <!-- 扫描 org.example 递归查找该包及子包的类 -->
    <context:component-scan base-package="org.example" />
</beans>
@Repository
public class BookDaoImpl implements BookDao { }

@Service("bookService")
public class BookServiceImpl implements BookService { }

public class App {
    public static void main(String[] args) {
        ConfigurableApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");

        BookService bookService = (BookService) ctx.getBean("bookService");
        System.out.println(bookService);

        BookDao bookDao = ctx.getBean(BookDao.class);
        System.out.println(bookDao);
    }
}

# 7.2. 纯注解开发模式

配置类 替代 配置文件:

@Configuration
@ComponentScan("org.example")
public class SpringConfig {
}

加载配置类:

public class AppForSpringConfig {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);

        BookService bookService = (BookService) ctx.getBean("bookService");
        System.out.println(bookService);

        BookDao bookDao = ctx.getBean(BookDao.class);
        System.out.println(bookDao);
    }
}

# 7.3. bean 作用范围与生命周期

作用范围:

  • @Scope("singleton") // "prototype"

生命周期:

  • @PostConstruct
  • @PreDestroy

示例:

@Repository
@Scope("singleton") // "prototype"
public class BookDaoImpl implements BookDao {
    public void save() {
        System.out.println("book dao save ...");
    }

    @PostConstruct // 构造器执行之后
    public void myInit() {
        System.out.println("init ...");
    }

    @PreDestroy // 销毁之前
    public void myDestroy() {
        System.out.println("destroy ...");
    }
}

public class App {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        System.out.println(ctx.getBean(BookDao.class));
        System.out.println(ctx.getBean(BookDao.class));
        ctx.close();
    }
}

# 7.4. 依赖注入

自动装配:

  • @Autowired@Qualifier
  • @Value

加载属性配置文件:

  • @PropertySource("jdbc.properties")

示例:

@Configuration
@ComponentScan("org.example")
@PropertySource("jdbc.properties") // src/main/resources/jdbc.properties
public class SpringConfig {
}

@Service
public class BookServiceImpl implements BookService {
    @Autowired // 引用注入
    // @Qualifier("bookDao") // 注入指定 id 的 bean,如 @Repository("bookDao")
    private BookDao bookDao;

    @Value("${jdbc.username}") // 值注入
    private String username;

    @Override
    public void save() {
        System.out.println("book service save ..." + username);
        bookDao.save();
    }
}


public class App {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        ctx.getBean(BookService.class).save();
    }
}

# 7.5. 管理第三方bean

第三方 bean 管理:

  • @Bean

第三方 bean 依赖注入:

  • @Import({JdbcConfig.class})
  • 简单类型:成员属性
  • 引用类型:方法形参

示例:

@Configuration
@ComponentScan("org.example")
@PropertySource("jdbc.properties") // src/main/resources/jdbc.properties
@Import({JdbcConfig.class})
public class SpringConfig {
}

public class JdbcConfig {
    @Value("${jdbc.username}") // 通过成员属性 注入 简单类型
    private String username;

    @Bean
    public DataSource dataSource(BookDao bookDao) { // 通过形参 注入 引用类型
        bookDao.save();

        DruidDataSource ds = new DruidDataSource();

        ds.setUsername(username);

        return ds;
    }
}

public class App {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        DataSource bean = ctx.getBean(DataSource.class);
        System.out.println(bean);
    }
}

# 8. Spring 整合

# 8.1. 配置集成 Mybatis

数据库:

CREATE DATABASE spring_db;

CREATE TABLE `spring_db`.`tbl_account`  (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(35) ,
  `money` double,
  PRIMARY KEY (`id`),
)

目录:

${root}/
    src/main/java
        org/example
            domain/
                Account.java
            dao/
                AccountDao.java
            service/
                AccountService.java
            service/impl/
                AccountServiceImpl.java
    resources/
        jdbc.properties
        SqlMapConfig.xml

pom.xml:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.2.10.RELEASE</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.16</version>
</dependency>
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.6</version>
</dependency>
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <version>8.3.0</version>
</dependency>

org.example.domain.Account:

public class Account implements Serializable {
    private Integer id;
    private String name;
    private Double money;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Double getMoney() {
        return money;
    }

    public void setMoney(Double money) {
        this.money = money;
    }

    @Override
    public String toString() {
        return "Account{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", money=" + money +
                '}';
    }
}

org.example.dao.AccountDao:

public interface AccountDao {
    @Insert("insert into tbl_account(name,money)values(#{name},#{money})")
    void save(Account account);

    @Delete("delete from tbl_account where id = #{id} ")
    void delete(Integer id);

    @Update("update tbl_account set name = #{name} , money = #{money} where id = #{id} ")
    void update(Account account);

    @Select("select * from tbl_account")
    List<Account> findAll();

    @Select("select * from tbl_account where id = #{id} ")
    Account findById(Integer id);
}

org.example.service.AccountService:

public interface AccountService {
    void save(Account account);

    void delete(Integer id);

    void update(Account account);

    List<Account> findAll();

    Account findById(Integer id);
}

org.example.service.impl.AccountServiceImpl:

@Service
public class AccountServiceImpl implements AccountService {
    @Autowired
    private AccountDao accountDao;

    public void save(Account account) {
        accountDao.save(account);
    }

    public void update(Account account){
        accountDao.update(account);
    }

    public void delete(Integer id) {
        accountDao.delete(id);
    }

    public Account findById(Integer id) {
        return accountDao.findById(id);
    }

    public List<Account> findAll() {
        return accountDao.findAll();
    }
}

src/main/resources/jdbc.properties:

jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/spring_db?useSSL=false
jdbc.username=root
jdbc.password=123456

src/main/resources/SqlMapConfig.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <!--读取外部properties配置文件-->
    <properties resource="jdbc.properties"/>
    <!--别名扫描的包路径-->
    <typeAliases>
        <package name="org.example.domain"/>
    </typeAliases>
    <!--数据源-->
    <environments default="mysql">
        <environment id="mysql">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="${jdbc.driver}"/>
                <property name="url" value="${jdbc.url}"/>
                <property name="username" value="${jdbc.username}"/>
                <property name="password" value="${jdbc.password}"/>
            </dataSource>
        </environment>
    </environments>
    <!--映射文件扫描包路径-->
    <mappers>
        <package name="org.example.dao"/>
    </mappers>
</configuration>

App.java:

public class App {
    public static void main(String[] args) throws IOException {
        // 1. 创建SqlSessionFactoryBuilder对象
        SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();

        // 2. 加载SqlMapConfig.xml配置文件
        InputStream inputStream = Resources.getResourceAsStream("SqlMapConfig.xml");

        // 3. 创建SqlSessionFactory对象
        SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(inputStream);

        // 4. 获取SqlSession
        SqlSession sqlSession = sqlSessionFactory.openSession();

        // 5. 执行SqlSession对象执行查询,获取结果User
        AccountDao accountDao = sqlSession.getMapper(AccountDao.class);
        Account ac = accountDao.findById(1);
        System.out.println(ac);

        // 6. 释放资源
        sqlSession.close();
    }
}

# 8.2. 注解集成 Mybatis

新增文件的目录:

${root}/
    src/main/java
        org/example
            config/
                JdbcConfig.java
                MybatisConfig.java
                SpringConfig.java
    AppForAnnotation.java

依赖:

<dependency>
    <!--Spring 操作数据库需要该 jar 包-->
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>5.2.10.RELEASE</version>
</dependency>
<dependency>
    <!--
    Spring与Mybatis整合的jar包
    这个jar包mybatis在前面,是Mybatis提供的
    -->
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>1.3.0</version>
</dependency>

代码:

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;

    @Bean
    public DataSource dataSource(){
        DruidDataSource ds = new DruidDataSource();
        ds.setDriverClassName(driver);
        ds.setUrl(url);
        ds.setUsername(userName);
        ds.setPassword(password);
        return ds;
    }
}


public class MybatisConfig {
    // 定义bean,SqlSessionFactoryBean,用于产生 SqlSessionFactory 对象
    @Bean
    public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){
        SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
        // 设置模型类的别名扫描
        ssfb.setTypeAliasesPackage("org.example.domain");
        // 设置数据源
        ssfb.setDataSource(dataSource);
        return ssfb;
    }

    // 定义 bean,返回 MapperScannerConfigurer 对象
    @Bean
    public MapperScannerConfigurer mapperScannerConfigurer(){
        MapperScannerConfigurer msc = new MapperScannerConfigurer();
        msc.setBasePackage("org.example.dao");
        return msc;
    }
}


@Configuration
@ComponentScan("org.example")
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class, MybatisConfig.class})
public class SpringConfig {
}


public class AppForAnnotation {
    public static void main(String[] args) throws IOException {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        AccountService accountService = ctx.getBean(AccountService.class);
        Account ac = accountService.findById(2);
        System.out.println(ac);
    }
}

# 8.3. 注解集成 Junit

目录:

${root}/
    src/test/java/
        org/example/service/
            AccountServiceTest.java

依赖:

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>5.2.10.RELEASE</version>
</dependency>

AccountServiceTest.java:

// 设置类运行器
@RunWith(SpringJUnit4ClassRunner.class)
// 设置 Spring 环境对应的配置类
@ContextConfiguration(classes = {SpringConfig.class}) //加载配置类
public class AccountServiceTest {
    //支持自动装配注入bean
    @Autowired
    private AccountService accountService;

    @Test
    public void testFindById(){
        System.out.println(accountService.findById(1));
    }
    @Test
    public void testFindAll(){
        System.out.println(accountService.findAll());
    }
}

# 9. AOP

# 9.1. AOP 简介

作用: 在不惊动原始设计的基础上为其进行功能增强,前面咱们有技术就可以实现这样的功能即代理模式。

Spring 的理念:无入侵式/无侵入式

# 9.2. AOP 入门案例

需求: 使用 SpringAOP 的注解方式完成在方法执行的前打印出当前系统时间。

依赖:

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.4</version>
</dependency>

目录:

${root}/
    src/main/java
        org/example/
            aop/
                MyAdvice.java
            config/
                SpringConfig.java
            dao/
                BookDao.java
                impl/BookDaoImpl.java
        App.java

代码:

public interface BookDao {
  public void save();
}


@Repository
public class BookDaoImpl implements BookDao {
  public void save() {
      System.out.println("book dao save ...");
  }
}


@Component
@Aspect
public class MyAdvice {
    // 切入点 BookDao.save()
    @Pointcut("execution(void org.example.dao.BookDao.save())")
    private void pt(){}

    // 在切入点 pt() 之前执行
    @Before("pt()")
    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}


@Configuration
@ComponentScan("org.example")
@EnableAspectJAutoProxy // 开启注解格式AOP功能
public class SpringConfig {
}


public class App {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        BookDao bookDao = ctx.getBean(BookDao.class);
        bookDao.save();
    }
}

# 9.3. AOP 工作流程

在创建通知类的对象时,对那些使用了切入点的 bean, 创建代理对象。

# 9.4. AOP 切入点表达式

# 9.4.1. 语法格式

execution(public User com.itheima.service.UserService.findById(int))
  • execution:动作关键字,描述切入点的行为动作,例如execution表示执行到指定切入点
  • public:访问修饰符,还可以是public,private等,可以省略
  • User:返回值,写返回值类型
  • com.itheima.service:包名,多级包使用点连接
  • UserService:类/接口名称
  • findById:方法名
  • int:参数,直接写参数的类型,多个类型用逗号隔开
  • 异常名:方法定义中抛出指定异常,可以省略
public class MyAdvice {

    // 匹配实现类的方法
    // @Pointcut("execution(void org.example.dao.impl.BookDaoImpl.save())")

    // (推荐)匹配接口的方法
    // @Pointcut("execution(void org.example.dao.BookDao.save())")
    private void pt(){}
}

# 9.4.2. 通配符

*: 表示 1个或多个

// 任意类型 org.example.1个任意包名.BookDao.find*(1个任意的参数类型)
@Pointcut("execution(* org.example.*.BookDao.find*(*))")

..:表示 0个或多个

// 任意类型 org.0个或多个任意包名.BookDao.findById(0个或多个参数)
@Pointcut("execution(* org..BookDao.findById(..))")

# 9.4.3. 书写技巧

对于切入点表达式的编写其实是很灵活的,那么在编写的时候,有没有什么好的技巧让我们用用:

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

# 9.5. AOP 通知类型

共提供了5种通知类型:

  • 前置通知
  • 后置通知
  • 环绕通知(重点)
  • 返回后通知(了解)
  • 抛出异常后通知(了解)

(1)前置通知,追加功能到方法执行前,类似于在代码1或者代码2添加内容

(2)后置通知,追加功能到方法执行后,不管方法执行的过程中有没有抛出异常都会执行,类似于在代码5添加内容

(3)返回后通知,追加功能到方法执行后,只有方法正常执行结束后才进行,类似于在代码3添加内容,如果方法执行抛出异常,返回后通知将不会被添加

(4)抛出异常后通知,追加功能到方法抛出异常后,只有方法执行出异常才进行,类似于在代码4添加内容,只有方法抛出异常后才会被添加

(5)环绕通知,环绕通知功能比较强大,它可以追加功能到方法执行的前后,这也是比较常用的方式,它可以实现其他四种通知类型的功能,具体是如何实现的,需要我们往下学习。

示例:

@Repository
public class BookDaoImpl implements BookDao {
    public void save() {
      System.out.println("book dao save ...");
    }

    @Override
    public int selectById(String id) {
        System.out.println("book dao selectById ..." + id);
        return 100;
    }
}


@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void org.example.dao.BookDao.save())")
    private void savePt(){}

    @Pointcut("execution(int org.example.dao.BookDao.selectById(String))")
    private void selectByIdPt(){}

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

    @After("selectByIdPt()")
    public void after(){
        System.out.println("after ...");
    }

    @Around("selectByIdPt()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("before ...");

        // 如果不调用 pjp.proceed(),则原始方法不执行
        Object ret = pjp.proceed();

        System.out.println("after ...");

        return ret;
    }

    // 没抛异常才执行
    @AfterReturning("selectByIdPt()")
    public void afterReturning(){
        System.out.println("afterReturning ...");
    }

    // 抛异常才执行
    @AfterThrowing("selectByIdPt()")
    public void afterThrowing(){
        System.out.println("afterThrowing ...");
    }
}

# 9.6. 案例: 业务层接口万次执行效率

@Component
@Aspect
public class ProjectAdvice {
    // 配置业务层的所有方法
    @Pointcut("execution(* org.example.service.*Service.*(..))")
    private void servicePt(){}
    
    @Around("servicePt()")
    public void runSpeed(){
        // 获取执行签名信息
        Signature signature = pjp.getSignature();

        // 通过签名获取执行操作名称(接口名)
        String className = signature.getDeclaringTypeName();

        // 通过签名获取执行操作名称(方法名)
        String methodName = signature.getName();
        
        long start = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
           pjp.proceed();
        }
        
        long end = System.currentTimeMillis();

        System.out.println("万次执行:" 
            + className + "." + methodName 
            + "---->" + (end-start) + "ms");
    } 
}

# 9.7. AOP 通知获取数据

获取参数:

  • JoinPoint:适用于前置、后置、返回后、抛出异常后通知
  • ProceedingJoinPoint:适用于环绕通知

获取返回值:

  • 返回后通知
  • 环绕通知

获取异常:

  • 抛出异常后通知
  • 环绕通知

# 9.7.1. 获取参数

非环绕通知获取方式:

@Before("pt()")
public void before(JoinPoint jp) 
    Object[] args = jp.getArgs();

    System.out.println(Arrays.toString(args));
    
    System.out.println("before advice ..." );
}

环绕通知获取方式:

// ProceedingJoinPoint 是 JoinPoint 的子类
@Around("pt()")
public Object around(ProceedingJoinPoint pjp)throws Throwable {
    Object[] args = pjp.getArgs();

    System.out.println(Arrays.toString(args));

    Object ret = pjp.proceed();
    
    return ret;
}

在环绕通知中,修改参数:

/*
有两个 pjp.proceed() 方法:

* pjp.proceed()
* pjp.proceed(Object[] objects)

调用无参数的 proceed,当原始方法有参数,会在调用的过程中自动传入参数

但是当需要修改原始方法的参数时,就只能采用带有参数的方法,如下:
*/ 
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable{
    Object[] args = pjp.getArgs();
    
    System.out.println(Arrays.toString(args));

    args[0] = 666;

    Object ret = pjp.proceed(args);
    
    return ret;
}

// 有了这个特性后,我们就可以在环绕通知中对原始方法的参数进行拦截过滤,
// 避免由于参数的问题导致程序无法正确运行,保证代码的健壮性。

# 9.7.2. 获取返回值

环绕通知获取返回值:

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

返回后通知获取返回值:

/*
returning = "形参名称"

如果形参中有 JoinPoint 类型,则该形参必须是第一个参数
*/
@AfterReturning(value = "pt()", returning = "ret")
public void afterReturning(JoinPoint jp, Object ret) {
    System.out.println("afterReturning advice ..." + ret);
}

# 9.7.3. 获取异常

环绕通知获取异常:

@Around("pt()")
public Object around(ProceedingJoinPoint pjp){
    Object ret = null;
    
    try {
        ret = pjp.proceed();
    } catch (Throwable throwable){
        t.printStackTrace();
    }

    return ret;
}

抛出异常后通知获取异常:

/*
throwing = "形参名称"
*/
@AfterThrowing(value = "pt()",throwing = "t")
public void afterThrowing(Throwable t) {
    System.out.println("afterThrowing advice ..." + t);
}

# 9.8. 案例: 去处字符串参数前后空白字符

@Component
@Aspect
public class DataAdvice {
    @Pointcut("execution(* org.example.service.*Service.*(..))")
    private void servicePt(){}
    
    @Around("DataAdvice.servicePt()")
    // @Around("servicePt()") 这两种写法都对
    public Object trimStr(ProceedingJoinPoint pjp) throws Throwable {
        // 获取原始方法的参数
        Object[] args = pjp.getArgs();
        
        for (int i = 0; i < args.length; i++) {
            // 判断参数是不是字符串
            if (args[i].getClass().equals(String.class)) {
                args[i] = args[i].toString().trim();
            }
        }
        
        // 将修改后的参数传入到原始方法的执行中
        Object ret = pjp.proceed(args);
        
        return ret;
    }
}

# 9.9. AOP 事务管理

# 9.9.1. Spring 事务简介

事务作用:在数据层保障一系列的数据库操作同成功同失败

Spring 事务作用:在数据层或业务层保障一系列的数据库操作同成功同失败

为什么业务层也需要处理事务呢?

举个简单的例子:

  • 转账业务会有两次数据层的调用,一次是加钱一次是减钱
  • 把事务放在数据层,加钱和减钱就有两个事务
  • 没办法保证加钱和减钱同时成功或者同时失败
  • 这个时候就需要将事务放在业务层进行处理

Spring 为了管理事务,提供了一个平台事务管理器 PlatformTransactionManager

public interface PlatformTransactionManager extends TransactionManager {
	void commit(TransactionStatus status) throws TransactionException;
	void rollback(TransactionStatus status) throws TransactionException;
}

commit 是用来提交事务, rollback 是用来回滚事务。

PlatformTransactionManager 只是一个接口,Spring 还为其提供了一个具体的实现:

public class DataSourceTransactionManager 
    extends AbstractPlatformTransactionManager 
    implements ResourceTransactionManager, InitializingBean {
    // ...
}

从名称上可以看出,我们只需要给它一个 DataSource 对象,它就可以帮你去在业务层管理事务。

其内部采用的是 JDBC 的事务,如果你持久层采用的是 JDBC 相关的技术,就可以采用这个事务管理器来管理你的事务。

而 Mybatis 内部采用的就是 JDBC 的事务,所以后期我们 Spring 整合 Mybatis 就采用的这个 DataSourceTransactionManager 事务管理器。

# 9.9.2. 开启事务

步骤 1: 开启事务注解

@Configuration
@ComponentScan("org.example")
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class, MybatisConfig.class})

// 开启注解式事务驱动
@EnableTransactionManagement 
public class SpringConfig {
}

步骤 2: 在 JdbcConfig 类中配置事务管理器

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;

    @Bean
    public DataSource dataSource() {
        DruidDataSource ds = new DruidDataSource();
        ds.setDriverClassName(driver);
        ds.setUrl(url);
        ds.setUsername(userName);
        ds.setPassword(password);
        return ds;
    }

    // 配置事务管理器,mybatis 使用的是 jdbc 事务
    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource){
        DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
        transactionManager.setDataSource(dataSource);
        return transactionManager;
    }
}

步骤 3: 在需要被事务管理的方法上添加注解

@Service
public class AccountServiceImpl implements AccountService {
    @Autowired
    private AccountDao accountDao;
    
    // 该方法开启事务
	@Transactional
    public void transfer(String out,String in ,Double money) {
        accountDao.outMoney(out,money);
        int i = 1/0;
        accountDao.inMoney(in,money);
    }

}

@Transactional 可以写在 接口类上、接口方法上、实现类上、实现类方法上:

  • 写在接口类上,该接口的所有实现类的所有方法都会有事务
  • 写在接口方法上,该接口的所有实现类的该方法都会有事务
  • 写在实现类上,该类中的所有方法都会有事务
  • 写在实现类方法上,该方法上有事务
  • 建议写在实现类或实现类的方法上

# 9.9.3. Spring 事务角色

事务管理员:发起事务方,在 Spring 中通常指代业务层开启事务的方法

事务协调员:加入事务方,在 Spring 中通常指代数据层方法,也可以是业务层方法

# 9.9.4. Spring 事务属性

@Transactional 的属性

readOnly:

  • true 只读事务,false 读写事务,增删改要设为 false, 查询设为 true。

timeout:

  • 设置超时时间单位秒,在多长时间之内事务没有提交成功就自动回滚,-1 表示不设置超时时间。

rollbackFor:

  • Spring 的事务只会对 ErrorRuntimeException 及其子类进行事务回滚,也就是说编译期异常默认不回滚
  • 此属性可以设置 非 ErrorRuntimeException 的异常也可以回滚
@Transactional(rollbackFor = {IOException.class})
public void transfer(String out,String in ,Double money) throws IOException {
    accountDao.outMoney(out,money);

    // int i = 1 / 0;

    if (true) throw new IOException();

    accountDao.inMoney(in,money);
}

noRollbackFor:

  • 当出现指定异常不进行事务回滚

rollbackForClassName:

  • 等同于 rollbackFor , 只不过属性为异常的类全名字符串

noRollbackForClassName:

  • 等同于 noRollbackFor ,只不过属性为异常的类全名字符串

isolation:

  • DEFAULT :默认隔离级别, 会采用数据库的隔离级别
  • READ_UNCOMMITTED : 读未提交
  • READ_COMMITTED : 读已提交
  • REPEATABLE_READ : 重复读取
  • SERIALIZABLE: 串行化

propagation:

  • 事务的传播行为

# 9.9.5. 事务传播行为

image-20250724231915857

# 9.9.6. 案例: 转账业务追加日志案例

需求:实现任意两个账户间转账操作,并对每次转账操作在数据库进行留痕

需求微缩:A账户减钱,B账户加钱,数据库记录日志

image-20250724232111773

@Repository
public interface LogDao {
    @Insert("insert into tbl_log (info,createDate) values(#{info},now())")
    void log(String info);
}

public interface LogService {
    void log(String out, String in, Double money);
}

@Service
public class LogServiceImpl implements LogService {
    @Autowired
    private LogDao logDao;

    // 开启新事物,而非加入 事务管理员的事务
	@Transactional(propagation = Propagation.REQUIRES_NEW) 
    public void log(String out,String in,Double money ) {
        logDao.log("转账操作由" + out + "到" + in + ",金额:" + money);
    }
}

@Service
public class AccountServiceImpl implements AccountService {
    @Autowired
    private AccountDao accountDao;

    @Autowired
    private LogService logService;

    @Transactional
    public void transfer(String out,String in ,Double money) {
        try {
            accountDao.outMoney(out,money);
            int i = 1 / 0;
            accountDao.inMoney(in,money);
        } finally {
            logService.log(out,in,money);
        }
    }
}
本章目录