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
,就需要让BookServiceImpl
和BookDaoImpl
也参与测试。这样的测试方式不仅耗时,而且会导致测试结果不稳定。因为
BookServiceImpl
和BookDaoImpl
的实现可能会影响BookControllerImpl
的测试结果。
至此,IoC 的想法已经呼之欲出了:将对象的创建、配置和管理交给容器,使其与对象的使用解耦。
- 对于资源浪费的问题,IoC 容器默认使用单例模式,保证只有一个实例
- 对于耦合度高的问题,当需要更换实现类时,只需要修改配置文件中对应的一行代码
- 对于初始化和配置麻烦的问题,只需要在配置文件中配置一次,容器会自动读取配置文件并创建对象
- 对于测试困难的问题,只需要将测试对象注入到容器中,容器会自动创建依赖的对象
这让我想到了前端常用的状态管理库,例如 Redux 和 Pinia 等。这些库的核心思想也是如此:将对象的创建、配置和管理交给库,使其与对象的使用解耦。
IoC 容器
Spring 的 IoC 容器是一个对象工厂,负责创建、配置和管理对象。在 IoC 容器中,对象被称为 Bean。
Spring 提供了两种 IoC 容器:BeanFactory
和 ApplicationContext
。其中,BeanFactory
是 Spring 的基础容器,如果没有特殊需求一般不用;ApplicationContext
是 BeanFactory
的子接口,提供了更多的功能,一般使用 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
元素来注入title
和author
属性:<bean id="book" class="com.example.Book"> <property name="title" value="Spring"/> <property name="author" value="Rod Johnson"/> </bean>
Spring 会自动调用
Book
类的setTitle
和setAuthor
方法,将title
和author
属性注入到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
类的有参构造函数,将title
和author
参数传入。
这当中有一些细节需要注意:
-
特殊值注入
-
如果需要注入
null
,可以使用<null/>
元素:<property name="title"> <null/> </property>
-
如果字符串中包含特殊字符,可以使用
<![CDATA[]]>
来包裹:<property name="title"> <value><![CDATA[Spring & Hibernate]]></value> </property>
也可以使用 HTML 转义字符:
<property name="title"> <value>Spring & 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>
-
集合注入
如果需要注入集合,可以使用
list
、set
、map
、props
等元素:<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>
可以看到,
author
与Book
的属性名相同,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。
假设我们有两个实现类 BookServiceImpl
和 BookDaoImpl
:
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(); } }
这里的
bookDaoImpl
是BookDaoImpl
类的 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(); } }
这里的
bookDaoImpl
是BookDaoImpl
类的 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([访问修饰符] 返回类型 [包名.类名].方法名(参数) [异常])
- 访问修饰符:
public
、protected
、private
、省略 - 返回类型:
*
、具体类型 - 包名:
*
表示一层任意包,..
表示当前包及其子包。例如,*.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
,则只能进行查询操作,不能进行增删改操作 -
rollbackFor
、rollbackForClassName
、noRollbackFor
、noRollbackForClassName
设置哪些异常会回滚事务,哪些异常不会回滚事务。参数为
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(); } }
它可以支持
if
、choose
、when
、otherwise
、trim
、where
、set
、foreach
等标签。 -
批量操作
@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