Ch3nyang's blog collections_bookmark

post

person

about

category

category

local_offer

tag

rss_feed

rss

深入 Java | (4)
快速入手 Spring

calendar_month 2024-02
archive 编程
tag java tag spring

There are 4 posts in series 深入 Java.

Spring 是一个开源的轻量级 JavaEE 框架。它的核心是控制反转(IoC)和面向切面编程(AOP)。Spring 的 IoC 容器负责管理 JavaBean 的生命周期,而 AOP 容器负责管理切面。Spring 还提供了一系列的模块,如 Spring MVC、Spring JDBC、Spring Security 等。

IoC

为什么需要 IoC

在实际开发中,常常会用到三层架构。三层架构是一种软件开发的设计模式,它将应用程序分为三个主要层次:

  • 控制器层(Controller):负责与用户进行交互,展示数据,并接收用户输入。例如网页前端、移动应用的用户界面等。
  • 业务逻辑层(Service):处理应用程序的核心功能和业务规则。例如应用程序的中间层代码,包含处理用户请求、验证输入、执行算法等逻辑。
  • 持久层(Dao):负责与数据库或其他数据存储系统进行交互。例如数据访问对象,负责执行数据库查询、插入、更新和删除操作的代码。

假设我们在开发一个网上书店系统,可以将系统划分为以下三个层次:

  • 控制器层:负责显示商品列表、搜索结果、商品详情页等。

    public class BookControllerImpl implements BookController {
        private BookService bookService = new BookServiceImpl();
    
        public void listBooks() {
            List<Book> books = bookService.listBooks();
              
            for (Book book : books) {
                System.out.println(book);
            }
        }
    }
    
  • 业务逻辑层:处理用户搜索商品、添加商品到购物车、下单等操作。

    public class BookServiceImpl implements BookService {
        private BookDao bookDao = new BookDaoImpl();
    
        public List<Book> listBooks() {
            return bookDao.listBooks();
        }
    }
    
  • 持久层:与数据库交互,查询商品信息、保存订单信息等。

    public class BookDaoImpl implements BookDao {
        public List<Book> listBooks() {
            // 查询数据库,返回商品列表
            return new ArrayList<>();
        }
    }
    

在逻辑上,这三层应该是一个倒金字塔型:大量控制器层调用少量业务逻辑层,持久层最少:

倒金字塔型

这样的写法有几个问题:

  • 资源浪费

    在实际实现中,由于每个 BookControllerImpl 都使用 new 创建了新的 BookServiceImpl 实例、每个 BookServiceImpl 都使用 new 创建了新的 BookDaoImpl 实例,导致了每个控制器层组件都挂了一个金字塔型的结构:

    金字塔型

    这造成了极大的浪费——因为我们知道,BookDaoImpl 很可能只需要一个实例就够用,而不是每个 BookServiceImpl 都创建一个实例。

    该问题可以通过单例模式来解决:也就是相当于创建一个类,该类保存全局变量。然而,这一方法将会引入更高的复杂性。

  • 耦合度高

    由于每个层次都直接依赖于下一层次,导致了耦合度过高。假如 BookDaoImpl 需要更换为 BookDaoAnotherImpl,那么每一个原来依赖于 BookDaoImpl 的类都需要进行修改。

  • 初始化和配置麻烦

    由于每个层次都需要手动创建下一层次的实例,导致了初始化和配置的麻烦。例如配置一个 JDBC 需要:

    dataSource = new DataSource();
    dataSource.setUrl("jdbc:mysql://localhost:3306/bookstore");
    dataSource.setUsername("root");
    dataSource.setPassword("password");
    dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
    

    目前的方法下,每个 BookDaoImpl 都需要写这么长一串,而且如果数据库地址、用户名、密码等信息发生变化,每个 BookDaoImpl 都需要修改。

  • 测试困难

    由于每个层次都直接依赖于下一层次,导致了测试困难。例如,如果要测试 BookControllerImpl,就需要让 BookServiceImplBookDaoImpl 也参与测试。

    这样的测试方式不仅耗时,而且会导致测试结果不稳定。因为 BookServiceImplBookDaoImpl 的实现可能会影响 BookControllerImpl 的测试结果。

至此,IoC 的想法已经呼之欲出了:将对象的创建、配置和管理交给容器,使其与对象的使用解耦。

  • 对于资源浪费的问题,IoC 容器默认使用单例模式,保证只有一个实例
  • 对于耦合度高的问题,当需要更换实现类时,只需要修改配置文件中对应的一行代码
  • 对于初始化和配置麻烦的问题,只需要在配置文件中配置一次,容器会自动读取配置文件并创建对象
  • 对于测试困难的问题,只需要将测试对象注入到容器中,容器会自动创建依赖的对象

这让我想到了前端常用的状态管理库,例如 Redux 和 Pinia 等。这些库的核心思想也是如此:将对象的创建、配置和管理交给库,使其与对象的使用解耦。

IoC 容器

Spring 的 IoC 容器是一个对象工厂,负责创建、配置和管理对象。在 IoC 容器中,对象被称为 Bean。

Spring 提供了两种 IoC 容器:BeanFactoryApplicationContext。其中,BeanFactory 是 Spring 的基础容器,如果没有特殊需求一般不用;ApplicationContextBeanFactory 的子接口,提供了更多的功能,一般使用 ApplicationContext

例如,我们有一个 Book 类:

package com.example;

public class Book {
    private String title;
    private String author;

    public Book() {
    }

    public Book(String title, String author) {
        this.title = title;
        this.author = author;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }

    @Override
    public String toString() {
        return "Book{" +
                "title='" + title + '\'' +
                ", author='" + author + '\'' +
                '}';
    }
}

在未使用 IoC 容器时,我们需要手动创建 Book 对象:

package com.example;

public class TestBook {
    @Test
    public void testBook() {
        Book book = new Book("Spring", "Rod Johnson");
        System.out.println(book);
    }
}

使用 IoC 容器后,我们则需要完成以下步骤:

  • 创建一个配置文件。例如叫 Beans.xml,配置 Book 类:

    <?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="book" class="com.example.Book">
            <property name="title" value="Spring"/>
            <property name="author" value="Rod Johnson"/>
        </bean>
      
    </beans>
    
  • 创建一个 ApplicationContext 对象,并从配置文件中读取 book 对象:

    package com.example;
    
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    
    public class TestBook {
        @Test
        public void testBook() {
            ApplicationContext context = new ClassPathXmlApplicationContext("Beans.xml");
            Book book = (Book) context.getBean("book");
            System.out.println(book);
        }
    }
    

这样,我们就使用 IoC 容器创建了 Book 对象。

依赖注入

依赖注入(Dependency Injection,DI)是 IoC 的一种实现方式。它主要有两种方式:构造器注入和属性注入。

  • 属性注入

    属性注入是通过属性的 setter 方法来注入依赖的。例如,在上一节的例子中,我们通过 property 元素来注入 titleauthor 属性:

    <bean id="book" class="com.example.Book">
        <property name="title" value="Spring"/>
        <property name="author" value="Rod Johnson"/>
    </bean>
    

    Spring 会自动调用 Book 类的 setTitlesetAuthor 方法,将 titleauthor 属性注入到 Book 对象中。

  • 构造器注入

    构造器注入是通过构造器来注入依赖的。上面的例子中我们定义了有参构造函数 Book(String title, String author),可以通过 constructor-arg 元素来注入依赖:

    <bean id="book" class="com.example.Book">
        <constructor-arg name="title" value="Spring"/>
        <constructor-arg name="author" value="Rod Johnson"/>
    </bean>
    

    Spring 会自动调用 Book 类的有参构造函数,将 titleauthor 参数传入。

这当中有一些细节需要注意:

  • 特殊值注入

    • 如果需要注入 null,可以使用 <null/> 元素:

      <property name="title">
          <null/>
      </property>
      
    • 如果字符串中包含特殊字符,可以使用 <![CDATA[]]> 来包裹:

      <property name="title">
          <value><![CDATA[Spring & Hibernate]]></value>
      </property>
      

      也可以使用 HTML 转义字符:

      <property name="title">
          <value>Spring &amp; Hibernate</value>
      </property>
      
  • 引用注入

    如果需要注入另一个 Bean,可以使用 ref 属性:

    <bean id="author" class="com.example.Author">
        <property name="name" value="Rod Johnson"/>
    </bean>
    
    <bean id="book" class="com.example.Book">
        <property name="title" value="Spring"/>
        <property name="author" ref="author"/>
    </bean>  
    

    这也可以写成:

    <bean id="book" class="com.example.Book">
        <property name="title" value="Spring"/>
        <property name="author">
            <ref bean="author"/>
        </property>
    </bean>
    

    当然,也可以直接写在内部:

    <bean id="book" class="com.example.Book">
        <property name="title" value="Spring"/>
        <property name="author">
            <bean class="com.example.Author">
                <property name="name" value="Rod Johnson"/>
            </bean>
        </property>
    </bean>
    
  • 集合注入

    如果需要注入集合,可以使用 listsetmapprops 等元素:

    <bean id="book" class="com.example.Book">
        <property name="authors">
            <list>
                <value>Rod Johnson</value>
                <value>Juergen Hoeller</value>
                <value>Keith Donald</value>
            </list>
        </property>
    </bean>
    
    <bean id="book" class="com.example.Book">
        <property name="authors">
            <set>
                <value>Rod Johnson</value>
                <value>Juergen Hoeller</value>
                <value>Keith Donald</value>
            </set>
        </property>
    </bean>
    
    <bean id="book" class="com.example.Book">
        <property name="authors">
            <map>
                <entry key="Rod Johnson" value="Spring"/>
                <entry key="Juergen Hoeller" value="Spring Boot"/>
                <entry key="Keith Donald" value="Spring Cloud"/>
            </map>
        </property>
    </bean>
    
    <bean id="book" class="com.example.Book">
        <property name="authors">
            <props>
                <prop key="Rod Johnson">Spring</prop>
                <prop key="Juergen Hoeller">Spring Boot</prop>
                <prop key="Keith Donald">Spring Cloud</prop>
            </props>
        </property>
    </bean>
    
  • p 命名空间

    Spring 提供了 p 命名空间,可以简化属性注入。

    首先需要在配置文件中引入 p 命名空间:

    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:p="http://www.springframework.org/schema/p"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    

    然后可以使用 p 命名空间来注入属性:

    <bean id="book" class="com.example.Book" p:title="Spring" p:author="Rod Johnson"/>
    

    这等价于:

    <bean id="book" class="com.example.Book">
        <property name="title" value="Spring"/>
        <property name="author" value="Rod Johnson"/>
    </bean>
    

    对于使用 ref 属性的情况,也可以使用 p 命名空间:

    <bean id="book" class="com.example.Book" p:title="Spring" p:author-ref="author"/>
    

自动装配

自动装配(Autowiring)是 Spring 的另一种实现方式。它可以自动识别 Bean 之间的依赖关系,从而省去了手动配置 Bean 之间的依赖关系。

Spring 提供了以下几种自动装配的方式:

  • no

    默认值,不自动装配。需要手动配置 Bean 之间的依赖关系。

  • byName

    根据 Bean 的名称自动装配。Spring 会自动查找与属性名相同的 Bean,并将其注入。例如:

    <bean id="author" class="com.example.Author">
        <property name="name" value="Rod Johnson"/>
    </bean>
    
    <bean id="book" class="com.example.Book">
        <property name="author" ref="author"/>
        <property name="title" value="Spring"/>
    </bean>
    

    最后一条可以被写为:

    <bean id="book" class="com.example.Book" autowire="byName">
        <property name="title" value="Spring"/>
    </bean>
    

    可以看到,authorBook 的属性名相同,Spring 会自动查找 author Bean,并将其注入到 Book 对象中。

  • byType

    根据 Bean 的类型自动装配。Spring 会自动查找与属性类型相同的 Bean,并将其注入。

    同样的,最后一条可以被写为:

    <bean id="book" class="com.example.Book" autowire="byType">
        <property name="title" value="Spring"/>
    </bean>
    

    如果有多个 Bean 的类型相同,Spring 会抛出异常。可以使用 @Primary 注解来指定首选 Bean。

  • constructor

    根据构造器参数类型自动装配。Spring 会自动查找与构造器参数类型相同的 Bean,并将其注入。

    例如,Book 类有一个构造器 Book(Author author, String title),可以写为:

    <bean id="book" class="com.example.Book" autowire="constructor">
        <constructor-arg value="Spring"/>
    </bean>
    

    Spring 会自动查找 Author 类型的 Bean,并将其注入到 Book 对象中。

基于注解的配置

以上我们都在使用 XML 文件来配置 Bean,Spring 也支持使用注解来配置 Bean。

假设我们有两个实现类 BookServiceImplBookDaoImpl

public class BookServiceImpl implements BookService {
    private BookDao bookDao = new BookDaoImpl();

    public List<Book> listBooks() {
        return bookDao.listBooks();
    }
}
public class BookDaoImpl implements BookDao {
    public List<Book> listBooks() {
        // 查询数据库,返回商品列表
        return new ArrayList<>();
    }
}

其中,BookServiceImpl 依赖于 BookDaoImpl。为了实现 IoC,首先需要在配置文件中配置自动扫描:

<?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">
  
      <context:component-scan base-package="com.example"/>

</beans>

然后可以使用注解,注册为 Bean:

@Service
public class BookServiceImpl implements BookService {
    private BookDao bookDao = new BookDaoImpl();

    public List<Book> listBooks() {
        return bookDao.listBooks();
    }
}
@Repository
public class BookDaoImpl implements BookDao {
    public List<Book> listBooks() {
        // 查询数据库,返回商品列表
        return new ArrayList<>();
    }
}

这里有两个需要说明的地方:

  • @Component 注解是 Spring 的通用注解,可以用于任何类。Spring 还提供了一些更具体的注解,如 @Repository@Service@Controller 等,分别用于持久层、业务逻辑层、控制器层
  • 生成的 Bean 的名称默认为类名的首字母小写,可以使用 @Component(value = "book") 来指定 Bean 的名称

属性的注入可以使用注解。例如,要想将 BookDaoImpl 注入到 BookServiceImpl 中,有这样几种方法:

  • @Autowired 属性注入

    @Service
    public class BookServiceImpl implements BookService {
        @Autowired
        private BookDao bookDao;
    
        public List<Book> listBooks() {
            return bookDao.listBooks();
        }
    }
    
  • @Autowired setter 方法注入

    @Service
    public class BookServiceImpl implements BookService {
        private BookDao bookDao;
    
        @Autowired
        public void setBookDao(BookDao bookDao) {
            this.bookDao = bookDao;
        }
    
        public List<Book> listBooks() {
            return bookDao.listBooks();
        }
    }
    
  • @Autowired 构造器注入

    @Service
    public class BookServiceImpl implements BookService {
        private BookDao bookDao;
    
        @Autowired
        public BookServiceImpl(BookDao bookDao) {
            this.bookDao = bookDao;
        }
    
        public List<Book> listBooks() {
            return bookDao.listBooks();
        }
    }
    

    如果写在形参上也是可以的:

    @Service
    public class BookServiceImpl implements BookService {
        private BookDao bookDao;
    
        public BookServiceImpl(@Autowired BookDao bookDao) {
            this.bookDao = bookDao;
        }
    
        public List<Book> listBooks() {
            return bookDao.listBooks();
        }
    }
    

    如果只有一个构造器,@Autowired 可以省略。

  • @Qualifier 注解注入

    @Autowired 默认按类型注入,如果有多个 Bean 的类型相同(例如 BookDao 接口有多个实现),可以使用 @Qualifier 注解来指定 Bean 的名称:

    @Service
    public class BookServiceImpl implements BookService {
        @Autowired
        @Qualifier("bookDaoImpl")
        private BookDao bookDao;
    
        public List<Book> listBooks() {
            return bookDao.listBooks();
        }
    }
    

    这里的 bookDaoImplBookDaoImpl 类的 Bean 名称。

  • @Resource 注解注入

    @Autowired 不同,@Resource 注解是 JDK 扩展包中的,它默认按名称注入,如果找不到名称则按照类型注入。它被用在属性或者 setter 方法上:

    @Service
    public class BookServiceImpl implements BookService {
        @Resource(name = "bookDaoImpl")
        private BookDao bookDao;
    
        public List<Book> listBooks() {
            return bookDao.listBooks();
        }
    }
    

    这里的 bookDaoImplBookDaoImpl 类的 Bean 名称。

  • @Value 注解注入

    @Value 注解可以用来注入基本类型、String 类型、数组、集合等。例如:

    @Component
    public class Book {
        @Value("Spring")
        private String title;
    
        @Value("Rod Johnson")
        private String author;
    
        @Value("${book.price}")
        private double price;
    
        @Value("${book.authors}")
        private String[] authors;
    
        @Value("#{${book.authors}}")
        private List<String> authorsList;
    }
    

    这里有几个需要注意的地方:

    • 如果需要注入的是一个基本类型,可以直接写在 @Value 注解中
    • 如果需要注入的是一个 SpEL 表达式,可以使用 #{} 包裹,它会在运行时计算表达式的值,通常用于数组、集合等
    • 如果需要注入的是一个外部配置文件中的值,可以使用 ${} 包裹,它会在运行时读取配置文件中的值

以上我们依然用到了部分 XML 配置,Spring 也提供了完全基于注解的配置。例如,我们可以使用 @Configuration 注解来标记配置类:

@Configuration
@ComponentScan(basePackages = "com.example")
public class AppConfig {
}

然后在使用 Bean 时加载配置类:

public class TestBook {
    @Test
    public void testBook() {
        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        Book book = context.getBean(Book.class);
        System.out.println(book);
    }
}

这样,我们就完全使用注解来配置 Bean 了。

AOP

为什么需要 AOP

我们再次回顾一下三层架构:

倒金字塔型

在实际开发中,我们的业务逻辑并不是完全自上而下的,日志、事务、权限控制等横向贯穿在各个层次之间。这些辅助功能被成为切面(Aspect)。

在严格的 OOP 中,如果要向业务代码中添加日志功能,需要这样做:

@Service
public class CalculatorServiceImpl implements CalculatorService {
    @Autowired
    private CalculatorDao calculatorDao;

    public int add(int a, int b) {
        System.out.println("[INFO] add method start");
        int result = calculatorDao.add(a, b);
        System.out.println("[INFO] add method end");
        return result;
    }

    public int subtract(int a, int b) {
        System.out.println("[INFO] subtract method start");
        int result = calculatorDao.subtract(a, b);
        System.out.println("[INFO] subtract method end");
        return result;
    }

    /* ... */
}

可以看到,这样做有几个问题:

  • 代码冗余

    每个方法都需要写高度雷同日志代码,导致了代码冗余。

  • 耦合度高

    业务代码和日志代码耦合在一起,导致了耦合度过高,不利于集中维护。

  • 横切关注点

    日志代码是横切关注点,它贯穿在各个方法中,但是却和业务逻辑无关。

AOP 的目的就是解决这些问题。它将横切关注点从业务代码中剥离出来,使得业务代码更加简洁、清晰。

它和 Python 中的装饰器有点类似,都是对原先的代码进行进一步包装,在其运行前、运行后、运行中额外执行一些代码。

AOP 术语

  • 横切关注点(Cross-cutting Concern)

    横切关注点是指那些和业务逻辑无关,但是贯穿在各个方法中的代码。例如日志、事务、权限控制等。

  • 通知(Advice)

    增强是指在横切关注点中需要执行的代码。例如输出日志、开启事务、检查权限等。它有以下几种类型:

    • 前置通知(Before):在目标方法执行前执行
    • 后置通知(After):在目标方法执行后执行
    • 返回通知(After Returning):在目标方法返回结果后执行
    • 异常通知(After Throwing):在目标方法抛出异常后执行
    • 环绕通知(Around):在目标方法执行前后执行,包括了前面四种通知
  • 切面(Aspect)

    切面是横切关注点和通知结合而成的类。

  • 目标对象(Target)

    目标对象是被增强的对象。例如 CalculatorServiceImpl

  • 代理对象(Proxy)

    代理对象是 Spring 生成的代理对象,它将目标对象和切面结合在一起。

  • 连接点(Join Point)

    连接点是指在程序执行过程中能够插入切面的点。例如方法调用、方法执行、异常抛出等。

  • 切入点(Pointcut)

    切入点是指用于定位连接点的表达式。

静态代理和动态代理

在讲解 AOP 之前,我们先来看看代理模式。

在静态代理中,代理类和目标类实现了同一个接口,代理类中持有目标类的引用,通过调用目标类的方法来实现代理:

public interface CalculatorService {
    int add(int a, int b);
}

public class CalculatorServiceImpl implements CalculatorService {
    public int add(int a, int b) {
        return a + b;
    }
}

public class CalculatorServiceProxy implements CalculatorService {
    private CalculatorService calculatorService;

    public CalculatorServiceProxy(calculatorService) {
        this.calculatorService = calculatorService;
    }

    public int add(int a, int b) {
        System.out.println("[INFO] add method start");
        int result = calculatorService.add(a, b);
        System.out.println("[INFO] add method end");
        return result;
    }
}

public class TestCalculator {
    @Test
    public void testCalculator() {
        CalculatorService calculatorService = new CalculatorServiceImpl();
        CalculatorService calculatorServiceProxy = new CalculatorServiceProxy(calculatorService);
        int result = calculatorServiceProxy.add(1, 2);
    }
}

这种方法只能代理一个接口,如果有多个接口需要代理,就需要写多个代理类。

在动态代理中,代理类和目标类没有实现同一个接口,代理类通过实现 InvocationHandler 接口来实现代理:

public interface CalculatorService {
    int add(int a, int b);
}

public class CalculatorServiceImpl implements CalculatorService {
    public int add(int a, int b) {
        return a + b;
    }
}

public class CalculatorServiceProxy implements InvocationHandler {
    private Object target;

    public CalculatorServiceProxy(Object target) {
        this.target = target;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("[INFO] " + method.getName() + " method start, args: " + Arrays.toString(args));
        Object result = method.invoke(target, args);
        System.out.println("[INFO] " + method.getName() + " method end");
        return result;
    }
}

public class TestCalculator {
    @Test
    public void testCalculator() {
        CalculatorService calculatorService = new CalculatorServiceImpl();
        CalculatorService calculatorServiceProxy = (CalculatorService) Proxy.newProxyInstance(
                calculatorService.getClass().getClassLoader(),
                calculatorService.getClass().getInterfaces(),
                new CalculatorServiceProxy(calculatorService)
        );
        int result = calculatorServiceProxy.add(1, 2);
    }
}

这里的 Proxy.newProxyInstance 方法会返回一个代理对象,它实现了 CalculatorService 接口,通过 CalculatorServiceProxy 来实现代理。

在 Spring 中,AOP 使用的就是动态代理。它通过将以上内容封装进一个类,来实现代理。

AOP 使用

Spring 提供了两种 AOP 使用方式:基于 XML 配置和基于注解配置。我们先来看看基于注解配置的方式。

首先,我们需要在配置类中开启 AOP:

@Configuration
@EnableAspectJAutoProxy
@ComponentScan(basePackages = "com.example")
public class AppConfig {
}

或者在 XML 配置文件中开启 AOP:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:aop="http://www.springframework.org/schema/aop"
        xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
  
      <aop:aspectj-autoproxy/>
      <context:component-scan base-package="com.example"/>

</beans>

然后,我们需要定义一个切面类。为了讲解,我们将各种通知都实现了一下:

@Aspect
@Component
public class LogAspect {
    @Before("execution(* com.example.CalculatorService.*(..))")
    public void before(JoinPoint joinPoint) {
        System.out.println("[INFO] " + joinPoint.getSignature().getName() + " method start, args: " + Arrays.toString(joinPoint.getArgs()));
    }

    @After("execution(* com.example.CalculatorService.*(..))")
    public void after(JoinPoint joinPoint) {
        System.out.println("[INFO] " + joinPoint.getSignature().getName() + " method end");
    }

    @AfterReturning(pointcut = "execution(* com.example.CalculatorService.*(..))", returning = "result")
    public void afterReturning(JoinPoint joinPoint, Object result) {
        System.out.println("[INFO] " + joinPoint.getSignature().getName() + " method return " + result);
    }

    @AfterThrowing(pointcut = "execution(* com.example.CalculatorService.*(..))", throwing = "e")
    public void afterThrowing(JoinPoint joinPoint, Throwable e) {
        System.out.println("[ERROR] " + joinPoint.getSignature().getName() + " method throw " + e);
    }

    @Around("execution(* com.example.CalculatorService.*(..))")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        Object result = null;

        try {
            System.out.println("[INFO] " + joinPoint.getSignature().getName() + " method start, args: " + Arrays.toString(joinPoint.getArgs()));
            result = joinPoint.proceed();
            System.out.println("[INFO] " + joinPoint.getSignature().getName() + " method return " + result);
        } catch (Throwable e) {
            System.out.println("[ERROR] " + joinPoint.getSignature().getName() + " method throw " + e);
            throw e;
        } finally {
            System.out.println("[INFO] " + joinPoint.getSignature().getName() + " method end");
        }

        return result;
    }
}

然后,CalculatorServiceImpl 中什么也不用改,就能正常输出日志了:

@Service
public class CalculatorServiceImpl implements CalculatorService {
    public int add(int a, int b) {
        int result = a + b;
        return result;
    }

    public int subtract(int a, int b) {
        int result = a - b;
        return result;
    }

    /* ... */
}

这样,我们就实现了 AOP。

对于有多个切面的情况,可以使用 @Order 注解来指定切面的优先级:

@Aspect
@Component
@Order(1)
public class LogAspect {
    /* ... */
}

@Aspect
@Component
@Order(2)
public class TransactionAspect {
    /* ... */
}

这样,TransactionAspect 的优先级高于 LogAspect,会先执行 TransactionAspect

切入点表达式

在上面的例子中,我们使用了切入点表达式来定位连接点。切入点表达式是一个字符串,它有以下几种:

  • execution:用于匹配方法执行的连接点

    语法:execution([访问修饰符] 返回类型 [包名.类名].方法名(参数) [异常])

    • 访问修饰符:publicprotectedprivate、省略
    • 返回类型:*、具体类型
    • 包名:* 表示一层任意包,.. 表示当前包及其子包。例如,*.example 表示某一层包下的 example 包,com.example..* 表示 com.example 包及其子包下的所有类
    • 类名:* 表示任意类,*Service 表示以 Service 结尾的类
    • 方法名:* 表示任意方法,add* 表示以 add 开头的方法
    • 参数:(..) 表示任意参数,(*) 表示一个参数,(*, *) 表示两个参数,(*, String) 表示第一个参数任意,第二个参数为 String 类型
  • within:用于匹配指定类型内的方法执行连接点

    语法:within([包名.类名])

  • this:用于匹配当前代理对象类型的方法执行连接点

    语法:this([包名.类名])

  • target:用于匹配当前目标对象类型的方法执行连接点

    语法:target([包名.类名])

  • args:用于匹配参数类型的方法执行连接点

    语法:args([参数类型])

  • @annotation:用于匹配标注了指定注解的方法执行连接点

    语法:@annotation([注解类型])

  • bean:用于匹配指定 Bean 的方法执行连接点

    语法:bean([Bean 名称])

切入点表达式也可以一次定义,多次使用:

@Pointcut("execution(* com.example.CalculatorService.*(..))")
public void pointcut() {}

@Before("pointcut()")
public void before(JoinPoint joinPoint) {
    System.out.println("[INFO] " + joinPoint.getSignature().getName() + " method start, args: " + Arrays.toString(joinPoint.getArgs()));
}

如果要在不同的切面中使用,可以:

@Before("com.example.LogAspect.pointcut()")
public void before(JoinPoint joinPoint) {
    System.out.println("[INFO] " + joinPoint.getSignature().getName() + " method start, args: " + Arrays.toString(joinPoint.getArgs()));
}

基于 XML 配置

AOP 同样能够基于 XML 配置:

<bean id="logAspect" class="com.example.LogAspect"/>

<aop:config>
    <aop:aspect ref="logAspect">
        <aop:pointcut id="pointcut" expression="execution(* com.example.CalculatorService.*(..))"/>
        <aop:before method="before" pointcut-ref="pointcut"/>
        <aop:after method="after" pointcut-ref="pointcut"/>
        <aop:after-returning method="afterReturning" pointcut-ref="pointcut" returning="result"/>
        <aop:after-throwing method="afterThrowing" pointcut-ref="pointcut" throwing="e"/>
        <aop:around method="around" pointcut-ref="pointcut"/>
    </aop:aspect>
</aop:config>

Transaction & JDBC

JDBC

在使用 JDBC 之前,我们需要先配置 jdbc.properties 文件:

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

然后,配置 XML 文件:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:context="http://www.springframework.org/schema/context"
        xmlns:jdbc="http://www.springframework.org/schema/jdbc"
        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
        http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd">
  
      <context:property-placeholder location="classpath:jdbc.properties"/>

      <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
          <property name="driverClassName" value="${jdbc.driver}"/>
          <property name="url" value="${jdbc.url}"/>
          <property name="username" value="${jdbc.username}"/>
          <property name="password" value="${jdbc.password}"/>
      </bean>

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

</beans>

也可以使用 Java 配置:

@Configuration
@ComponentScan(basePackages = "com.example")
public class AppConfig {
    @Bean
    public DataSource dataSource() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/bookstore");
        dataSource.setUsername("root");
        dataSource.setPassword("password");
        return dataSource;
    }

    @Bean
    public JdbcTemplate jdbcTemplate() {
        JdbcTemplate jdbcTemplate = new JdbcTemplate();
        jdbcTemplate.setDataSource(dataSource());
        return jdbcTemplate;
    }
}

然后,我们可以使用 JdbcTemplate 来操作数据库:

@Repository
public class BookDaoImpl implements BookDao {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    public void addBook(Book book) {
        String sql = "INSERT INTO book VALUES(?, ?)";
        jdbcTemplate.update(sql, book.getTitle(), book.getAuthor());
    }

    public void updateBookAuthorByTitle(String title, String author) {
        String sql = "UPDATE book SET author = ? WHERE title = ?";
        jdbcTemplate.update(sql, author, title);
    }

    public Book getBookByTitle(String title) {
        String sql = "SELECT * FROM book WHERE title = ?";
        return jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(Book.class), title);
    }

    public List<Book> getAllBooks() {
        String sql = "SELECT * FROM book";
        return jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(Book.class));
    }

    public int countBooks() {
        String sql = "SELECT COUNT(*) FROM book";
        return jdbcTemplate.queryForObject(sql, Integer.class);
    }
}

为什么需要事务

在实际开发中,我们经常会遇到这样的情况:一个业务操作需要执行多个 SQL 语句,如果其中一个 SQL 语句执行失败,那么其他 SQL 语句也应该回滚。例如,购书时,需要先扣除用户的余额,然后再减少书的库存。如果书没了,但是余额却扣除了,那么就会出现问题;同理,如果余额不够,书却减少了,也会出现问题。

在最原始的编程式事务管理中,我们需要先关闭自动提交,然后手动提交或回滚事务:

@Service
public class BookServiceImpl implements BookService {
    @Autowired
    private BookDao bookDao;

    public void buyBook(String title, String username) {
        Book book = bookDao.getBookByTitle(title);
        int price = book.getPrice();

        try {
            DataSourceUtils.getConnection(dataSource).setAutoCommit(false);

            bookDao.updateBookStockByTitle(title, -1);
            bookDao.updateUserBalanceByUsername(username, -price);

            DataSourceUtils.getConnection(dataSource).commit();
        } catch (Exception e) {
            DataSourceUtils.getConnection(dataSource).rollback();
        } finally {
            DataSourceUtils.getConnection(dataSource).setAutoCommit(true);
        }
    }
}

可以看到,这一方法实现了事务管理的 ACID 特性,但是这样做有几个问题:

  • 代码冗余

    每个方法都需要写高度雷同事务代码,导致了代码冗余。

  • 耦合度高

    业务代码和事务代码耦合在一起,导致了耦合度过高,不利于集中维护。

因此,我们需要事务管理来解决这些问题。

事务管理

Spring 提供了两种事务管理方式:编程式事务管理和声明式事务管理。声明式事务管理可以有效解决上面提到的问题。

首先,我们需要在配置文件中开启事务管理:

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

      <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
          <property name="driverClassName" value="${jdbc.driver}"/>
          <property name="url" value="${jdbc.url}"/>
          <property name="username" value="${jdbc.username}"/>
          <property name="password" value="${jdbc.password}"/>
      </bean>

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

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

      <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
          <property name="dataSource" ref="dataSource"/>
      </bean>

</beans>

也可以使用注解配置:

@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = "com.example")
public class AppConfig {
    @Bean
    public DataSource dataSource() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/bookstore");
        dataSource.setUsername("root");
        dataSource.setPassword("password");
        return dataSource;
    }

    @Bean
    public JdbcTemplate jdbcTemplate() {
        JdbcTemplate jdbcTemplate = new JdbcTemplate();
        jdbcTemplate.setDataSource(dataSource());
        return jdbcTemplate;
    }

    @Bean
    public DataSourceTransactionManager transactionManager() {
        DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
        transactionManager.setDataSource(dataSource());
        return transactionManager;
    }
}

然后,我们可以使用 @Transactional 注解来声明事务:

@Service
public class BookServiceImpl implements BookService {
    @Autowired
    private BookDao bookDao;

    @Transactional
    public void buyBook(String title, String username) {
        Book book = bookDao.getBookByTitle(title);
        int price = book.getPrice();

        bookDao.updateBookStockByTitle(title, -1);
        bookDao.updateUserBalanceByUsername(username, -price);
    }
}

这样,我们就实现了声明式事务管理。

@Transactional 注解有以下几个属性:

  • propagation

    事务传播行为,确定了有包含关系的两个事务如何执行,默认值为 Propagation.REQUIRED。它有以下几种取值:

    • Propagation.REQUIRED:如果当前没有事务,就新建一个事务;如果当前有事务,就加入到当前事务中
    • Propagation.SUPPORTS:如果当前有事务,就加入到当前事务中;如果当前没有事务,就以非事务方式执行
    • Propagation.MANDATORY:如果当前有事务,就加入到当前事务中;如果当前没有事务,就抛出异常
    • Propagation.REQUIRES_NEW:新建一个事务,如果当前有事务,就将当前事务挂起
    • Propagation.NOT_SUPPORTED:以非事务方式执行,如果当前有事务,就将当前事务挂起
    • Propagation.NEVER:以非事务方式执行,如果当前有事务,就抛出异常
    • Propagation.NESTED:如果当前没有事务,就新建一个事务;如果当前有事务,就在当前事务中嵌套一个事务
  • isolation

    事务隔离级别,确定了不同事务之间如何互相影响,默认值为 Isolation.DEFAULT。它有以下几种取值:

    • Isolation.DEFAULT:使用数据库默认的隔离级别
    • Isolation.READ_UNCOMMITTED:读未提交,即一个事务可以读取另一个事务已经修改但还未提交的数据
    • Isolation.READ_COMMITTED:读已提交,即一个事务只能读取另一个事务已经提交的数据
    • Isolation.REPEATABLE_READ:可重复读,即一个事务在多次读取同一数据时,读到的数据是一样的,在此期间,其他事务对该数据的修改是不可见的
    • Isolation.SERIALIZABLE:串行化,即一个事务在执行时,另一个事务不能对其进行修改
  • timeout

    事务超时时间,默认值为 -1,单位为秒。如果程序卡住,超过了指定时间,事务会自动回滚

  • readOnly

    是否只读事务,默认值为 false。如果设置为 true,则只能进行查询操作,不能进行增删改操作

  • rollbackForrollbackForClassNamenoRollbackFornoRollbackForClassName

    设置哪些异常会回滚事务,哪些异常不会回滚事务。参数为 Class 类型或者 String 类型

基于 XML 配置

声明式事务管理同样能够基于 XML 配置:

<tx:advice id="txAdvice" transaction-manager="transactionManager">
    <tx:attributes>
        <tx:method name="buy*" propagation="REQUIRED" isolation="DEFAULT" timeout="-1" read-only="false"/>
    </tx:attributes>
</tx:advice>

Spring MVC

为什么需要 Spring MVC

在实际开发中,我们经常会遇到这样的情况:用户请求一个 URL,服务器返回一个 HTML 页面。

这个过程中,我们需要处理用户请求、调用业务逻辑、返回 HTML 页面。如果没有框架,我们需要自己处理这些事情,这样会导致代码冗余、耦合度高、不利于维护。

Spring MVC 就是为了解决这些问题而生的。它将请求处理、业务逻辑、视图渲染分离开来,使得代码更加简洁、清晰。

MVC 是一种设计模式,它将应用程序分为三个部分:模型(Model)、视图(View)、控制器(Controller)。模型负责处理业务逻辑,视图负责渲染页面,控制器负责处理用户请求。

Spring MVC 使用

我们这里只介绍纯 Java 配置方法。Spring MVC 有着不一样的目录结构:

src/
  main/
    java/
      com/example/config/
        WebAppInitializer.java  # Servlet 容器初始化
        RootConfig.java         # 根容器配置(服务层、数据源等)
        WebConfig.java          # Web层配置(控制器、视图解析器等)
    webapp/
      WEB-INF/
        views/
          hello.jsp             # 视图文件
      index.jsp                 # 首页

首先编写 WebAppInitializer 类:

public class WebAppInitializer implements WebApplicationInitializer {
  @Override
  public void onStartup(ServletContext servletContext) {
    // 1. 创建根容器
    AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext();
    rootContext.register(RootConfig.class);

    // 2. 注册 ContextLoaderListener
    servletContext.addListener(new ContextLoaderListener(rootContext));

    // 3. 创建 Web 容器
    AnnotationConfigWebApplicationContext webContext = new AnnotationConfigWebApplicationContext();
    webContext.register(WebConfig.class);

    // 4. 配置 DispatcherServlet
    DispatcherServlet servlet = new DispatcherServlet(webContext);
    ServletRegistration.Dynamic registration = servletContext.addServlet("appServlet", servlet);
    registration.setLoadOnStartup(1);
    registration.addMapping("/");
  }
}

然后编写 Web 层配置类 WebConfig

@Configuration
@EnableWebMvc
@ComponentScan("com.example.controller")
public class WebConfig implements WebMvcConfigurer {

  // 视图解析器
  @Bean
  public ViewResolver viewResolver() {
    InternalResourceViewResolver resolver = new InternalResourceViewResolver();
    resolver.setPrefix("/WEB-INF/views/");
    resolver.setSuffix(".jsp");
    resolver.setExposeContextBeansAsAttributes(true);
    return resolver;
  }

  // 静态资源处理
  @Override
  public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
    configurer.enable();
  }
}

最后实现控制器类 HelloController

@Controller
public class HelloController {
  @RequestMapping("/hello")
  public String hello(Model model) {
    model.addAttribute("message", "Hello Spring MVC!");
    return "hello"; // 对应 /WEB-INF/views/hello.jsp
  }
}

hello.jsp 文件:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Hello</title>
</head>
<body>
    <h1>${message}</h1>
</body>
</html>

这样,我们就实现了一个简单的 Spring MVC。

请求处理

@Controller 注解用于标识控制器类,这会告诉 Spring 这是一个控制器类。

Spring MVC 使用 @RequestMapping 注解来处理请求。它有以下几种用法:

  • @RequestMapping 注解

    用于处理请求,可以用在类上或者方法上。它有以下几种属性:

    • value:请求 URL,可以是一个字符串或者字符串数组
    • method:请求方法,可以是一个 RequestMethod 枚举值或者枚举值数组
    • params:请求参数,可以是一个字符串数组
    • headers:请求头,可以是一个字符串数组
    • consumes:请求内容类型,可以是一个字符串数组
    • produces:响应内容类型,可以是一个字符串数组

    例如:

    @Controller
    @RequestMapping("/book")
    public class BookController {
      @RequestMapping(value = "/list", method = RequestMethod.GET)
      public String list(Model model) {
        return "list";
      }
    
      @RequestMapping(value = "/add", method = RequestMethod.POST)
      public String add(Book book) {
        return "redirect:/book/list";
      }
    }
    
  • @GetMapping@PostMapping@PutMapping@DeleteMapping 注解

    用于处理 GET、POST、PUT、DELETE 请求。它们是 @RequestMapping 的缩写,例如:

    @Controller
    @RequestMapping("/book")
    public class BookController {
      @GetMapping("/list")
      public String list(Model model) {
        return "list";
      }
    
      @PostMapping("/add")
      public String add(Book book) {
        return "redirect:/book/list";
      }
    }
    
  • @PathVariable 注解

    用于获取 URL 中的参数,例如:

    @GetMapping("/book/{id}")
    public String get(@PathVariable("id") int id, Model model) {
      Book book = bookService.getBookById(id);
      model.addAttribute("book", book);
      return "book";
    }
    
  • @RequestParam@RequestHeader@RequestBody 注解

    用于获取请求参数、请求头、请求体,例如:

    @GetMapping("/book")
    public String get(@RequestParam("id") int id, Model model) {
      Book book = bookService.getBookById(id);
      model.addAttribute("book", book);
      return "book";
    }
    
  • @ModelAttribute 注解

    用于将请求参数绑定到模型对象,例如:

    @PostMapping("/book")
    public String add(@ModelAttribute Book book) {
      bookService.addBook(book);
      return "redirect:/book/list";
    }
    
  • @SessionAttributes 注解

    用于将模型对象存储到会话中,例如:

    @Controller
    @RequestMapping("/book")
    @SessionAttributes("book")
    public class BookController {
      @GetMapping("/book")
      public String get(@RequestParam("id") int id, Model model) {
        Book book = bookService.getBookById(id);
        model.addAttribute("book", book);
        return "book";
      }
    
      @PostMapping("/book")
      public String add(@ModelAttribute Book book) {
        bookService.addBook(book);
        return "redirect:/book/list";
      }
    }
    
  • @ResponseBody 注解

    用于返回 JSON 数据,例如:

    @GetMapping("/book")
    @ResponseBody
    public Book get(@RequestParam("id") int id) {
      return bookService.getBookById(id);
    }
    

Spring Boot

为什么需要 Spring Boot

从前面的内容可以看出,Spring 配置繁琐,需要配置 XML 文件、Java 文件,需要配置很多东西。Spring Boot 就是为了解决这些问题而生的。

Spring Boot 是 Spring 的一个子项目,它简化了 Spring 应用的开发,它可以自动配置、内嵌服务器、无需 XML 配置文件。

Spring Boot 使用

Spring Boot 可以直接使用 Spring Initializr 来生成项目,生成时,选择 Spring Boot 版本、项目名称、项目类型、依赖等。

Spring Boot 项目的目录结构:

src/
  main/
    java/
      com/example/
        controller/
          BookController.java
        model/
          Book.java
        service/
          BookService.java
        Application.java
    resources/
      application.yml
      static/
        style.css
      templates/
        book.html

Spring Boot 配置

Spring Boot 使用 application.properties 或者 application.yml 来配置项目。

下面是一些常用的配置:

server:
  port: 8080 # 服务器端口
  servlet:
    context-path: /bookstore # 项目路径
logging:
  level:
    root: info # 日志级别
  file:
    name: app.log # 日志文件名
spring:
  datasource: # 数据源配置
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/book
    username: root
    password: password
  jpa: # JPA 配置
    hibernate:
      ddl-auto: update # 自动建表
    show-sql: true # 显示 SQL
  profiles:
    active: dev # 激活的配置文件

Spring Boot 启动类

Spring Boot 项目的启动类:

@SpringBootApplication
public class Application {
  public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
  }
}

@SpringBootApplication 注解是 Spring Boot 的核心注解,它包含了 @Configuration@EnableAutoConfiguration@ComponentScan 注解。

Spring Boot 控制器

Spring Boot 控制器:

@RestController
public class BookController {
  @Autowired
  private BookService bookService;

  @GetMapping("/book/{id}")
  public Book get(@PathVariable("id") int id) {
    return bookService.getBookById(id);
  }

  @PostMapping("/book")
  public void add(@RequestBody Book book) {
    bookService.addBook(book);
  }
}

这和 Spring MVC 的控制器类是一样的。

Spring Boot 服务层

Spring Boot 服务层:

@Service
public class BookService {
  @Autowired
  private BookDao bookDao;

  public Book getBookById(int id) {
    return bookDao.getBookById(id);
  }

  public void addBook(Book book) {
    bookDao.addBook(book);
  }
}

这就是基本的 Spring 的写法。

Spring Boot 数据库

Spring Boot 数据库配置:

@Configuration
public class DataSourceConfig {
  @Bean
  @ConfigurationProperties(prefix = "spring.datasource")
  public DataSource dataSource() {
    return DataSourceBuilder.create().build();
  }
}

Spring Boot 数据库访问:

@Repository
public class BookDao {
  @Autowired
  private JdbcTemplate jdbcTemplate;

  public Book getBookById(int id) {
    String sql = "SELECT * FROM book WHERE id = ?";
    return jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(Book.class), id);
  }

  public void addBook(Book book) {
    String sql = "INSERT INTO book VALUES(?, ?)";
    jdbcTemplate.update(sql, book.getTitle(), book.getAuthor());
  }
}

Spring Boot 静态资源

Spring Boot 静态资源:

@Configuration
public class WebConfig implements WebMvcConfigurer {
  @Override
  public void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
  }
}

这样,我们就实现了一个简单的 Spring Boot 项目。

MyBatis

为什么需要 MyBatis

在实际开发中,我们经常会遇到这样的情况:需要操作数据库,但是 JDBC 太底层,Spring JDBC 太繁琐。MyBatis 就是为了解决这些问题而生的。

MyBatis 是一个持久层框架,它将 SQL 语句和 Java 对象映射起来,使得操作数据库更加方便。

MyBatis 使用

MyBatis 使用 XML 文件或者注解来配置 SQL 语句。我们这里只介绍注解配置方法。

首先,我们需要在配置文件中配置数据源:

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/book
    username: root
    password: password

然后,我们需要配置 MyBatis:

@Configuration
@MapperScan("com.example.mapper")
public class MyBatisConfig {
  @Bean
  public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
    SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
    factoryBean.setDataSource(dataSource);
    return factoryBean.getObject();
  }
}

然后,我们需要定义一个映射器接口:

@Mapper
public interface BookMapper {
  @Select("SELECT * FROM book WHERE id = #{id}")
  Book getBookById(int id);

  @Insert("INSERT INTO book VALUES(#{id}, #{title}, #{author})")
  void addBook(Book book);
}

这个接口中定义了两个方法,一个用于查询,一个用于插入。它们分别使用了 @Select@Insert 注解。类似的,我们还可以使用 @Update@Delete 注解。注解中的 SQL 语句使用 #{} 或者 ${} 来引用参数。值得注意的是,#{} 使用的是占位符,通过预编译的方式来防止 SQL 注入,而 ${} 直接拼接参数,容易导致 SQL 注入。

然后,我们就可以使用映射器接口来操作数据库:

@Service
public class BookService {
  @Autowired
  private BookMapper bookMapper;

  public Book getBookById(int id) {
    return bookMapper.getBookById(id);
  }

  public void addBook(Book book) {
    bookMapper.addBook(book);
  }
}

这样,我们就实现了一个简单的 MyBatis。

MyBatis 高级用法

MyBatis 还有很多高级用法,例如:

  • 动态 SQL

    @SelectProvider(type = BookSqlProvider.class, method = "getBook")
    Book getBook(int id, String title);
    
    public class BookSqlProvider {
      public String getBook(int id, String title) {
        return new SQL() {{
          SELECT("*");
          FROM("book");
          if (id != 0) {
            WHERE("id = #{id}");
          }
          if (title != null) {
            WHERE("title = #{title}");
          }
        }}.toString();
      }
    }
    

    它可以支持 ifchoosewhenotherwisetrimwheresetforeach 等标签。

  • 批量操作

    @InsertProvider(type = BookSqlProvider.class, method = "addBooks")
    void addBooks(List<Book> books);
    
    public class BookSqlProvider {
      public String addBooks(Map<String, List<Book>> map) {
        List<Book> books = map.get("books");
        StringBuilder sql = new StringBuilder();
        sql.append("INSERT INTO book VALUES ");
        for (Book book : books) {
          sql.append("(").append(book.getId()).append(", '").append(book.getTitle()).append("', '").append(book.getAuthor()).append("'), ");
        }
        sql.delete(sql.length() - 2, sql.length());
        return sql.toString();
      }
    }
    
  • 缓存

    @CacheNamespace
    public interface BookMapper {
      @Select("SELECT * FROM book WHERE id = #{id}")
      @Options(useCache = true)
      Book getBookById(int id);
    }
    

    它可以支持 @CacheNamespace@Options 注解。

    当我们添加了 @CacheNamespace 注解后,MyBatis 会自动缓存查询结果,当我们再次查询相同的数据时,MyBatis 会直接从缓存中获取数据,而不会再次查询数据库。

MyBatis Plus

MyBatis Plus 是 MyBatis 的增强工具,它提供了很多增强功能,例如:

  • 代码生成器

    MyBatis Plus 提供了代码生成器,可以根据数据库表生成实体类、映射器接口、XML 文件。

    public class CodeGenerator {
      public static void main(String[] args) {
        AutoGenerator generator = new AutoGenerator();
        generator.setGlobalConfig(new GlobalConfig().setOutputDir(System.getProperty("user.dir") + "/src/main/java"));
        generator.setDataSource(new DataSourceConfig().setUrl("jdbc:mysql://localhost:3306/book").setUsername("root").setPassword("password"));
        generator.setPackageInfo(new PackageConfig().setParent("com.example").setModuleName("book"));
        generator.setStrategy(new StrategyConfig().setInclude("book"));
        generator.execute();
      }
    }
    

    运行这个代码,就可以生成实体类、映射器接口、XML 文件。

  • 分页插件

    MyBatis Plus 提供了分页插件,可以方便地进行分页查询。

    @GetMapping("/book")
    public Page<Book> list(@RequestParam("page") int page, @RequestParam("size") int size) {
      return bookService.listBooks(page, size);
    }
    
    @Service
    public class BookService {
      @Autowired
      private BookMapper bookMapper;
    
      public Page<Book> listBooks(int page, int size) {
        return bookMapper.selectPage(new Page<>(page, size), null);
      }
    }
    

    这里,我们使用了 Page 类来表示分页查询结果。MyBatis Plus 会自动查询总数,并返回分页查询结果。

  • 逻辑删除

    MyBatis Plus 提供了逻辑删除功能,可以方便地进行逻辑删除。

    @TableLogic
    private Integer deleted;
    
    @Delete("DELETE FROM book WHERE id = #{id}")
    void deleteBookById(int id);
    
    @Update("UPDATE book SET deleted = 1 WHERE id = #{id}")
    void deleteBookById(int id);
    

    这里,我们使用了 @TableLogic 注解来表示逻辑删除字段。MyBatis Plus 会自动将逻辑删除字段加入到查询条件中。

  • 多租户

    MyBatis Plus 提供了多租户功能,可以方便地进行多租户查询。

    @MultiTenant
    private String tenantId;
    
    @Select("SELECT * FROM book WHERE tenant_id = #{tenantId}")
    List<Book> listBooks(String tenantId);
    
    @InterceptorIgnore(tenantLine = "true")
    @Select("SELECT * FROM book")
    List<Book> listBooks();
    

    这里,我们使用了 @MultiTenant 注解来表示多租户字段。MyBatis Plus 会自动将多租户字段加入到查询条件中。

Spring Security

为什么需要 Spring Security

在实际开发中,我们经常会遇到这样的情况:需要对用户进行认证、授权。如果没有框架,我们需要自己处理这些事情,这样会导致代码冗余、耦合度高、不利于维护。Spring Security 就是为了解决这些问题而生的。

Spring Security 是一个安全框架,它提供了认证、授权、攻击防护等功能,使得应用程序更加安全。

Spring Security 使用

Spring Security 使用 Java 配置来配置项目:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
  @Autowired
  private UserService userService;

  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userService).passwordEncoder(new BCryptPasswordEncoder());
  }

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
      .antMatchers("/login").permitAll()
      .anyRequest().authenticated()
      .and()
      .formLogin().loginPage("/login").defaultSuccessUrl("/").permitAll()
      .and()
      .logout().permitAll();
  }
}

这个配置类继承了 WebSecurityConfigurerAdapter 类,它重写了 configure 方法,用于配置认证和授权。

configure(AuthenticationManagerBuilder auth) 方法用于配置认证,我们可以使用 auth.userDetailsService() 方法来配置用户认证服务,使用 auth.inMemoryAuthentication() 方法来配置内存用户认证服务。

configure(HttpSecurity http) 方法用于配置授权,我们可以使用 http.authorizeRequests() 方法来配置请求授权,使用 http.formLogin() 方法来配置表单登录,使用 http.logout() 方法来配置退出登录。

Comments

Share This Post