一、Bean
在Spring框架中,bean
是由Spring容器管理的一个对象。Bean是应用程序中的核心组件,Spring容器负责创建、初始化、配置以及管理这些bean的生命周期。Bean通常是通过配置文件(如XML或Java配置类)定义的。
为了更好地理解Spring中的bean,可以把Spring容器想象成一个智能工厂,bean就是工厂中生产出来的各种产品。工厂根据生产需求(配置文件)来制造产品,并且负责管理这些产品的整个生命周期,从生产、初始化、到使用和销毁。
通俗例子
假设我们有一家“蛋糕工厂”,这家工厂生产各种不同种类的蛋糕。我们可以把Spring中的bean类比成这些蛋糕,而Spring容器就是这家蛋糕工厂。
定义bean
首先,我们定义一种蛋糕类(bean):
public class Cake {
private String flavor;public Cake(String flavor) {
this.flavor = flavor;
}public String getFlavor() {
return flavor;
}public void setFlavor(String flavor) {
this.flavor = flavor;
}@Override
public String toString() {
return "Cake{" +
"flavor='" + flavor + '\'' +
'}';
}
}
配置bean
接着,我们在Spring配置文件中定义这个bean:
XML配置文件:
<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="chocolateCake" class="com.example.Cake">
<constructor-arg value="Chocolate"/>
</bean><bean id="vanillaCake" class="com.example.Cake">
<constructor-arg value="Vanilla"/>
</bean></beans>
Java配置类:
@Configuration
public class AppConfig {@Bean
public Cake chocolateCake() {
return new Cake("Chocolate");
}@Bean
public Cake vanillaCake() {
return new Cake("Vanilla");
}
}
使用bean
现在,我们可以从Spring容器中获取这些bean(蛋糕):
public class BakeryApp {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);Cake chocolateCake = context.getBean("chocolateCake", Cake.class);
Cake vanillaCake = context.getBean("vanillaCake", Cake.class);System.out.println(chocolateCake); // 输出:Cake{flavor='Chocolate'}
System.out.println(vanillaCake); // 输出:Cake{flavor='Vanilla'}
}
}
在这个例子中,Spring容器(蛋糕工厂)根据配置文件(生产需求)生产出两种不同的蛋糕bean(Chocolate和Vanilla),并管理它们的生命周期。通过这种方式,Spring框架帮助开发者更好地管理应用程序中的对象,提高了代码的可维护性和可扩展性。
Spring中的bean是单例的吗?
在Spring框架中,默认情况下,bean是单例的。这意味着在Spring容器中,对于每个定义的bean,容器只会创建一个实例,并且所有对这个bean的请求都会返回同一个实例。
什么是单例
单例(Singleton)是一种设计模式,它确保在应用程序的整个生命周期中,一个类只有一个实例,并提供一个全局访问点。换句话说,单例模式保证一个类只会被实例化一次,无论你多少次请求这个类的实例,始终得到的都是同一个对象。
"容器只会创建一个实例"的意思是,在整个应用程序运行期间,Spring框架只会为某个特定的bean创建一次对象,并且每次你需要这个bean时,Spring都会返回同一个对象,而不是每次都创建一个新的对象。
通俗的解释
想象你有一个大公司,里面有一个非常重要的打印机,所有员工都需要用这个打印机打印文件。如果公司决定只有一台打印机,那么每次员工要打印文件时,他们都会使用同一台打印机,而不是每个员工都买一台新的打印机。
在这个例子中:
- 打印机 就像是Spring中的一个bean。
- 公司 就像是Spring容器。
- 员工 就像是需要使用这个bean的各个部分。
无论多少个员工需要用打印机打印文件,他们都只会使用同一台打印机。这就意味着,这台打印机是唯一的、共享的,不会因为有新的打印需求而再去买一台新的打印机。
代码示例
假设我们在Spring容器中定义了一个Printer
类,然后使用它:
package com.example;
public class Printer {
// 打印方法
public void print(String message) {
System.out.println("打印消息: " + message);
}
}
Spring配置类:
package com.example;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class AppConfig {
@Bean
public Printer printer() {
return new Printer();
}
}
使用示例:
package com.example;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;public class Main {
public static void main(String[] args) {
// 创建Spring应用上下文,加载配置类
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);// 从上下文中获取单例bean实例
Printer printer1 = context.getBean(Printer.class);
Printer printer2 = context.getBean(Printer.class);// 打印消息
printer1.print("Hello, World!"); // 输出:打印消息: Hello, World!// 验证两个引用是否指向同一个对象
System.out.println(printer1 == printer2); // 输出:true,表明两个引用是同一个实例
}
}
在这个示例中:
- 当我们第一次调用
context.getBean(Printer.class)
时,Spring容器会创建一个Printer
实例。 - 当我们第二次调用
context.getBean(Printer.class)
时,Spring容器不会再创建一个新的Printer
实例,而是返回第一次创建的那个实例。
这就是“容器只会创建一个实例”的意思。它确保了在应用程序中,每次需要使用 Printer
时,都使用同一个 Printer
对象,而不是每次都创建一个新的对象。这有助于节省资源,统一管理,避免重复创建相同对象。
Spring Bean 是线程安全的吗?
在Spring中,默认情况下,bean是单例的,这意味着它们在Spring容器内是唯一的一个实例。单例bean在多线程环境下可能会出现线程安全问题,因为多个线程可能会同时访问同一个bean实例。如果bean实例是有状态的(即它包含可变的实例变量),那么在并发访问时就需要特别注意线程安全问题。
线程安全的考虑
- 无状态Bean:如果bean是无状态的(例如,所有的方法都是无副作用的,不会修改bean的内部状态),那么它是线程安全的,因为没有共享的可变状态。
- 有状态Bean:如果bean是有状态的(即包含可变的实例变量),则需要采取措施确保线程安全。这可以通过以下几种方式实现:
- 同步方法或代码块:在方法或代码块上使用同步锁,以确保同一时刻只有一个线程可以执行该方法或代码块。
- 使用线程安全的数据结构:使用Java并发包提供的线程安全数据结构,如
ConcurrentHashMap
等。 - 局部变量:尽量使用局部变量而不是实例变量,因为局部变量是线程私有的,不会有并发问题。
有状态的Bean包含可变的实例变量(即状态),这些实例变量在多个线程同时访问和修改时可能会导致数据不一致或产生竞态条件(race condition)。这是因为多个线程在并发修改这些共享变量时,会出现以下问题:
- 竞态条件:两个或多个线程同时读取、修改和写入共享变量,可能导致数据不一致。
- 可见性问题:一个线程对共享变量的修改对其他线程可能不可见,导致其他线程读取到过期的值。
- 原子性问题:对于复合操作(例如读取-修改-写入),如果不是原子性的,那么多个线程同时进行这些操作时可能导致错误的结果。
Bean的生命周期
在Spring框架中,Bean的生命周期和Bean定义(Bean Definition)是理解Spring管理和配置Bean的重要概念。以下是对这两个概念的详细解释。
Bean 的生命周期
Spring容器管理着Bean的整个生命周期,从创建到销毁。Bean的生命周期主要包括以下几个阶段:
-
实例化(Instantiation):
- Spring容器根据Bean定义创建Bean的实例。这个过程通常涉及调用Bean的构造函数来创建对象。
-
属性填充(Populate Properties):
- Spring容器将Bean定义中配置的属性值注入到Bean实例中。这些属性值可以是简单数据类型、其他Bean实例等。
-
初始化(Initialization):
@PostConstruct
注解:如果Bean类上有@PostConstruct
注解的方法,这些方法将在属性填充之后被调用。- 实现
InitializingBean
接口:如果Bean实现了InitializingBean
接口,其afterPropertiesSet()
方法将在属性填充之后被调用。 - 自定义初始化方法:在Bean定义中可以配置自定义的初始化方法,这些方法会在属性填充之后被调用。
-
使用(Use):
- Bean在容器中被使用。此阶段是Bean的实际使用阶段,可以处理业务逻辑。
-
销毁(Destruction):
@PreDestroy
注解:如果Bean类上有@PreDestroy
注解的方法,这些方法将在Bean被销毁之前被调用。- 实现
DisposableBean
接口:如果Bean实现了DisposableBean
接口,其destroy()
方法将在Bean被销毁之前被调用。 - 自定义销毁方法:在Bean定义中可以配置自定义的销毁方法,这些方法会在Bean销毁之前被调用。
import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.stereotype.Component; @Component public class MyBean implements InitializingBean, DisposableBean { @PostConstruct public void init() { System.out.println("Bean 初始化:@PostConstruct"); } @Override public void afterPropertiesSet() throws Exception { System.out.println("Bean 初始化:InitializingBean.afterPropertiesSet()"); } public void customInit() { System.out.println("Bean 初始化:自定义初始化方法"); } @PreDestroy public void cleanup() { System.out.println("Bean 销毁:@PreDestroy"); } @Override public void destroy() throws Exception { System.out.println("Bean 销毁:DisposableBean.destroy()"); } public void customDestroy() { System.out.println("Bean 销毁:自定义销毁方法"); } }
AOP(面向切面编程,Aspect-Oriented Programming)是一种编程范式,它使得我们能够将横切关注点(cross-cutting concerns)与业务逻辑分开,进而提高代码的模块化。横切关注点是那些影响多个模块的功能,例如日志记录、安全性、事务管理等。
通俗的例子
假设你有一个餐馆,里面有多种菜品。你要做的事情是为每道菜添加一些额外的配料,比如撒盐和胡椒。这些配料是所有菜品共享的额外步骤,但它们并不直接属于任何一道菜的主要配方。
如果你手动在每道菜的制作过程中添加这些配料,你的代码就会变得重复且难以维护。这时候你就可以使用AOP来解决这个问题。
二、AOP的主要概念
-
切面(Aspect):定义了横切关注点的实现,比如日志记录、权限检查等。在我们的餐馆例子中,"撒盐和胡椒"就可以看作是一个切面。
-
连接点(Join Point):程序中可以插入切面的具体位置,比如方法的调用点。在餐馆例子中,连接点是“制作菜品”的时刻。
-
通知(Advice):在连接点上执行的具体操作。比如,制作菜品时自动撒盐和胡椒。在代码中,它可以是"前置通知"(在连接点前执行)、"后置通知"(在连接点后执行)、"异常通知"(在连接点抛出异常时执行)等。
-
切入点(Pointcut):定义了哪些连接点会被切面(Aspect)拦截。比如,选择所有制作“主菜”的时刻来撒盐和胡椒。
-
织入(Weaving):将切面应用到目标对象的过程,生成最终的代理对象。这个过程可以在编译时、类加载时或运行时完成。在餐馆的例子中,就是将撒盐和胡椒的步骤加入到每道菜的制作过程中。
1、AOP记录操作日志
下面是一个使用Spring AOP记录操作日志的示例代码。我们将创建一个简单的Spring Boot应用,展示如何通过AOP记录方法调用的日志。
1. 创建Spring Boot应用
1.1. pom.xml
(Maven依赖配置)
首先,确保在pom.xml
中添加了Spring Boot和AOP相关的依赖:
<dependencies>
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Starter AOP -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- Spring Boot Starter Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
1.2. 业务逻辑类(目标对象)
package com.example;
import org.springframework.stereotype.Service;
@Service
public class UserService {
// 添加用户的方法
public void addUser(String username) {
System.out.println("用户 " + username + " 已添加。");
}
// 删除用户的方法
public void deleteUser(String username) {
System.out.println("用户 " + username + " 已删除。");
}
}
1.3. 切面(Aspect)
package com.example;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LoggingAspect {
// 在目标对象的所有方法执行后执行
@After("execution(* com.example.UserService.*(..))")
public void logAfterMethod(JoinPoint joinPoint) {
// 获取目标方法的名称
String methodName = joinPoint.getSignature().getName();
// 获取方法参数
Object[] args = joinPoint.getArgs();
// 打印日志
System.out.println("方法 " + methodName + " 执行完毕");
if (args.length > 0) {
System.out.println("参数: ");
for (Object arg : args) {
System.out.println(" " + arg);
}
}
}
}
2. 创建Spring Boot主程序
package com.example;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Bean
public CommandLineRunner demo(UserService userService) {
return args -> {
// 调用UserService的方法
userService.addUser("张三");
userService.deleteUser("李四");
};
}
}
运行结果
用户 张三 已添加。
方法 addUser 执行完毕
参数:
张三
用户 李四 已删除。
方法 deleteUser 执行完毕
参数:
李四
三、Spring中事务失效的场景
在Spring事务管理中,事务的回滚通常是由未被捕获的异常触发的。事务通知(Transaction advice)是通过AOP(面向切面编程)机制来管理事务的,事务的开始、提交和回滚都是由Spring的事务管理器通过切面通知来完成的。
当说“目标自己处理掉异常,事务通知无法知晓”时,意思是如果在业务方法中捕获并处理了异常,Spring的事务通知(即事务管理器)可能无法知道异常的发生,从而无法执行事务的回滚。
详细解释
事务通知与异常处理
-
事务通知的作用:
- Spring事务管理通常使用AOP来提供事务支持。当一个方法被标记为事务性(例如使用
@Transactional
注解),Spring会在方法执行前开启一个事务,并在方法执行后决定是提交还是回滚事务。 - 如果事务方法抛出一个运行时异常,Spring事务通知会捕获到这个异常,并执行回滚操作。
- Spring事务管理通常使用AOP来提供事务支持。当一个方法被标记为事务性(例如使用
-
异常被处理掉:
- 如果在事务方法内部捕获了异常并处理掉了(例如记录日志、展示错误消息),这个异常不会被重新抛出到方法的调用者处。由于Spring事务通知没有收到未处理的异常,它不会触发回滚操作。
- 处理异常的代码块中的异常被认为已经被处理完了,这使得事务管理器无法感知到事务的异常状态,因此无法执行回滚。
举个例子
示例代码:异常处理导致事务通知失效
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class MyService {
@Transactional
public void processTransaction() {
try {
// 执行可能抛出异常的操作
performOperation();
} catch (RuntimeException e) {
// 捕获异常并处理
System.out.println("捕获异常: " + e.getMessage());
// 异常被处理掉,没有重新抛出
}
// 继续执行其他逻辑
}
private void performOperation() {
// 模拟抛出异常
throw new RuntimeException("测试异常");
}
}
解决方法
-
重新抛出异常:
- 如果需要确保事务回滚,可以在处理异常时将其重新抛出,确保事务通知能够感知到异常,并执行回滚操作。
@Transactional
public void processTransaction() {
try {
performOperation();
} catch (RuntimeException e) {
System.out.println("捕获异常: " + e.getMessage());
// 重新抛出异常,确保事务能够回滚
throw e;
}
}
如果在Spring事务管理中抛出检查型异常(Checked Exception
),而没有正确处理,可能会导致事务管理失效,无法按照预期进行回滚。默认情况下,Spring事务管理器不会对检查型异常进行回滚,因此如果业务逻辑中抛出检查型异常,事务可能不会被回滚,导致数据的不一致性或其他问题。
事务失效的原因
-
默认行为:
- Spring事务管理器的默认行为是只对运行时异常(
RuntimeException
及其子类)进行回滚,而不对检查型异常(Exception
的子类,但不是RuntimeException
的子类)进行回滚。 - 这意味着,如果事务方法中抛出了一个检查型异常,Spring事务管理器不会自动回滚事务
- Spring事务管理器的默认行为是只对运行时异常(
运行时异常(Runtime Exception)
运行时异常是 RuntimeException
的子类。它们表示程序中的错误或异常情况,这些异常通常是由编程错误引起的,如空指针访问、数组越界等。这类异常通常是不可恢复的,因此Java设计决定将其标记为运行时异常,不强制要求在代码中进行捕获或处理。
-
示例:
NullPointerException
:尝试在一个为null
的对象上调用方法或访问字段时抛出。ArrayIndexOutOfBoundsException
:访问数组的非法索引时抛出。IllegalArgumentException
:传递给方法的参数不合法时抛出。ArithmeticException
:算术运算异常,如除以零。
检查型异常(Checked Exception)
检查型异常是 Exception
的子类,但不是 RuntimeException
的子类。它们通常表示程序在运行时可以预见并处理的异常,例如 I/O 错误、数据库访问错误等。Java 编译器要求程序员显式处理这些异常,否则编译将无法通过。
-
示例:
IOException
:I/O 操作失败时抛出,如文件未找到或读取错误。SQLException
:与数据库交互时发生的异常,如连接失败或查询错误。ClassNotFoundException
:试图加载一个不存在的类时抛出。FileNotFoundException
:文件操作失败时抛出,如文件不存在。
SpringMVC的运行流程(Model-View-Controller)
在基于前后端分离的Spring Boot应用中,前端和后端通过API进行通信。下面是一个通俗的例子,展示了Spring Boot的Spring MVC在前后端分离架构中的控制流程。
示例场景
假设我们正在开发一个简单的任务管理应用,用户可以查看任务列表和创建新任务。前端使用React开发,后端使用Spring Boot。
控制流程
-
用户请求
- 用户在浏览器中访问前端应用的任务列表页面(React应用)。
-
前端请求API
- 用户点击“查看任务”按钮,前端发送一个HTTP GET请求到后端API以获取任务列表。请求URL可能是
http://localhost:8080/api/tasks
。
- 用户点击“查看任务”按钮,前端发送一个HTTP GET请求到后端API以获取任务列表。请求URL可能是
// React组件中获取任务列表
import React, { useEffect, useState } from 'react';
import axios from 'axios';
function TaskList() {
const [tasks, setTasks] = useState([]);
useEffect(() => {
axios.get('/api/tasks')
.then(response => {
setTasks(response.data);
})
.catch(error => {
console.error('Error fetching tasks:', error);
});
}, []);
return (
<div>
<h1>Task List</h1>
<ul>
{tasks.map(task => (
<li key={task.id}>{task.name}</li>
))}
</ul>
</div>
);
}
export default TaskList;
-
DispatcherServlet
- Spring Boot的
DispatcherServlet
作为前端控制器,接收所有的HTTP请求。 DispatcherServlet
将请求路由到具体的处理器(控制器)。
- Spring Boot的
-
处理请求
DispatcherServlet
将请求分发给TaskController
。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/tasks")
public class TaskController {
@Autowired
private TaskService taskService;
@GetMapping
public List<Task> getAllTasks() {
return taskService.getAllTasks();
}
@PostMapping
public Task createTask(@RequestBody Task task) {
return taskService.createTask(task);
}
}
-
业务逻辑
- 控制器调用服务层(
TaskService
)来处理业务逻辑,例如获取任务列表或创建新任务。
- 控制器调用服务层(
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class TaskService {
@Autowired
private TaskRepository taskRepository;
public List<Task> getAllTasks() {
return taskRepository.findAll();
}
public Task createTask(Task task) {
return taskRepository.save(task);
}
}
-
数据库操作
- 服务层通过数据访问层(
TaskRepository
)与数据库交互,以获取或存储任务数据。
- 服务层通过数据访问层(
import org.springframework.data.jpa.repository.JpaRepository;
public interface TaskRepository extends JpaRepository<Task, Long> {
}
-
返回数据
- 控制器将任务列表数据以JSON格式返回给前端。
-
前端展示
- 前端接收到JSON数据后,更新React组件的状态,并重新渲染页面,以展示任务列表。
总结
在基于前后端分离的Spring Boot应用中,控制流程如下:
- 用户请求:用户在前端应用中进行操作(例如点击按钮)。
- 前端请求API:前端通过HTTP请求与后端API进行交互。
- DispatcherServlet:Spring Boot的前端控制器接收并处理请求。
- 处理请求:请求被路由到相应的控制器(
TaskController
)。 - 业务逻辑:控制器调用服务层(
TaskService
)处理业务逻辑。 - 数据库操作:服务层通过数据访问层(
TaskRepository
)与数据库交互。 - 返回数据:控制器将数据以JSON格式返回给前端。
- 前端展示:前端接收数据并更新页面内容。
这种流程确保了前端和后端的职责分离,使得应用程序的开发和维护更加高效和灵活。
Springboot自动配置原理
@SpringBootApplication
是一个用于启动Spring Boot应用的注解,它实际上是多个注解的组合,包括 @Configuration
、@EnableAutoConfiguration
和 @ComponentScan
。这些注解协同工作,实现了Spring Boot的自动配置和组件扫描功能。
/**
* Spring Boot应用的主类
*/
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
// 启动Spring Boot应用
SpringApplication.run(DemoApplication.class, args);
}
}
在Spring框架中,@Configuration
注解用于标记一个类作为配置类,这个类用于定义和配置Spring容器中的Bean。以下是对 @Configuration
注解及其功能和作用的详细解释:
@Configuration
的作用
-
声明配置类:
@Configuration
注解将一个普通的Java类标记为配置类。Spring容器会将这个类视为定义Spring Bean的源。
-
Bean定义:
- 在标记为
@Configuration
的类中,你可以使用@Bean
注解的方法来定义Spring容器中的Bean。这些方法会被调用,返回的对象将被注册为Spring容器中的Bean。
- 在标记为
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {
// 定义一个Bean,返回的对象将被Spring容器管理
@Bean
public MyService myService() {
return new MyService();
}
// 另一个Bean定义
@Bean
public MyRepository myRepository() {
return new MyRepository();
}
}
在Spring框架中,@Configuration
类中的 @Bean
方法返回的对象会被注册为Spring容器中的Bean,这些Bean可以在应用程序中以多种方式使用。以下是这些对象在Spring应用中可能的用法:
1. 依赖注入(Dependency Injection)
说明:Spring容器可以将注册的Bean自动注入到需要它们的地方。这是Spring框架的核心特性之一,允许将Bean的依赖关系声明在类中,Spring会在运行时自动注入这些依赖。
赖注入(Dependency Injection, DI)是Spring框架中的核心特性之一,它通过将对象的依赖关系从类的内部逻辑中解耦,简化了对象的创建和管理。以下是依赖注入的具体用途和通俗的例子:
依赖注入的用途
- 解耦合:通过将依赖关系注入到类中,避免了类与其依赖的紧密耦合,使得代码更灵活、更易于维护。
- 增强可测试性:可以方便地替换依赖项,用于单元测试时注入模拟对象或测试替代品。
- 简化配置:自动管理Bean的生命周期和依赖,简化了配置和管理。
- 提高可维护性:使得代码结构更加清晰,有助于遵循单一职责原则和依赖倒置原则。
通俗的例子
假设你有一个简单的应用,包含一个 Car
类和一个 Engine
类。Car
类依赖于 Engine
类,即 Car
需要一个 Engine
对象来工作。
传统的依赖管理
在传统的编程中,Car
类可能直接创建 Engine
对象,这会导致紧密耦合和难以测试的代码:
public class Engine {
public void start() {
System.out.println("Engine started");
}
}
public class Car {
private Engine engine;
public Car() {
// 直接创建Engine对象
this.engine = new Engine();
}
public void drive() {
engine.start();
System.out.println("Car is driving");
}
}
在这个例子中,Car
类直接创建了 Engine
实例,这意味着你无法轻松地替换 Engine
对象或进行单元测试。
使用依赖注入
使用Spring的依赖注入可以将 Engine
对象的创建和管理交给Spring容器,Car
类只需声明它的依赖。这样,你可以灵活地替换 Engine
实现,并更方便地进行测试:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
// 定义Engine类
@Component
public class Engine {
public void start() {
System.out.println("Engine started");
}
}
// 定义Car类,使用依赖注入
@Service
public class Car {
private final Engine engine;
// 构造函数注入Engine
@Autowired
public Car(Engine engine) {
this.engine = engine;
}
public void drive() {
engine.start();
System.out.println("Car is driving");
}
}
在没有依赖注入(DI)的情况下,Car
类直接创建和管理 Engine
对象,这会带来一些问题,特别是在替换对象或进行单元测试时:
1. 紧耦合
当 Car
类直接创建 Engine
对象时,它与 Engine
类紧密耦合。这意味着:
- 替换难度:如果你想要替换
Engine
类的实现(比如使用不同的引擎类型),你需要修改Car
类的代码。这样会导致代码的修改和维护变得复杂。 - 灵活性差:
Car
类无法在运行时根据需要选择不同的Engine
实现,无法根据不同的配置条件切换引擎。
2. 难以进行单元测试
单元测试是确保代码正确性的关键。直接创建依赖对象会使测试变得困难:
-
无法注入模拟对象:在单元测试中,你通常会使用模拟对象(mock objects)来替代真实的依赖对象,以便测试特定的逻辑。由于
Car
类直接创建Engine
对象,你无法轻松地注入一个模拟的Engine
对象来测试Car
类的逻辑。例如,你可能想测试
Car
的drive()
方法是否正确调用了Engine
的start()
方法。如果Engine
对象是直接创建的,你需要实际创建一个Engine
实例,这样会使得测试变得复杂和不灵活。
依赖注入的优势
使用依赖注入,Spring容器会负责创建和管理 Engine
对象,并将其注入到 Car
类中。这带来以下优势:
-
解耦合:
Car
类只依赖于Engine
接口(或者通过构造函数或字段注入),不需要知道Engine
的具体实现。这样你可以在配置文件或代码中灵活地指定不同的Engine
实现。
-
易于替换:
- 你可以轻松地在配置文件中更改
Engine
的实现,或在不同的环境中使用不同的实现,而无需修改Car
类的代码。
- 你可以轻松地在配置文件中更改
-
方便测试:
- 在单元测试中,你可以使用模拟对象替代实际的
Engine
实现。例如,你可以使用Mockito等框架来创建一个模拟的Engine
对象,并将其注入到Car
实例中。这使得测试更加灵活和容易管理。
- 在单元测试中,你可以使用模拟对象替代实际的
示例:使用依赖注入进行单元测试
假设我们使用Mockito来测试 Car
类:
import static org.mockito.Mockito.*;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
@SpringBootTest
public class CarTest {
@MockBean
private Engine engine; // 模拟Engine对象
@Autowired
private Car car; // 注入Car对象
@Test
public void testDrive() {
// 调用drive()方法
car.drive();
// 验证Engine的start()方法是否被调用
verify(engine).start();
}
}
在这个测试中,我们创建了一个模拟的 Engine
对象,并将其注入到 Car
类中。通过这种方式,我们可以测试 Car
类的逻辑,而不依赖于真实的 Engine
实现。这种解耦合的方式使得测试变得更加简单和灵活。
在Spring Boot中,@EnableAutoConfiguration
和 @ComponentScan
是两个重要的注解,它们有不同的功能和作用。让我们详细解释一下这两个注解的作用以及它们之间的关系。
@EnableAutoConfiguration
功能
@EnableAutoConfiguration
是 Spring Boot 提供的一个注解,用于启用 Spring Boot 的自动配置功能。它会根据项目的类路径、配置文件、Bean定义等信息自动配置Spring应用上下文。这使得开发人员可以快速构建一个可用的应用程序,而无需手动配置大量的Spring组件。
作用
- 自动配置:Spring Boot 会根据你添加的依赖项自动配置Spring应用。例如,如果你添加了Spring Data JPA的依赖,
@EnableAutoConfiguration
会自动配置数据源、EntityManagerFactory等。 - 默认配置:如果没有提供明确的配置,Spring Boot会使用默认的配置值,帮助简化应用程序的配置过程。
使用示例
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
在这个示例中,@SpringBootApplication
是一个复合注解,其中包含了 @EnableAutoConfiguration
注解。它启用了Spring Boot的自动配置功能,并配置了Spring应用上下文。
@ComponentScan
功能
@ComponentScan
注解用于指定Spring容器扫描组件的包路径。它告诉Spring容器去扫描指定包及其子包中的所有组件(如 @Component
、@Service
、@Repository
、@Controller
等),并将这些组件注册为Spring的Bean。
作用
- 扫描组件:自动检测和注册应用中的Spring组件,确保它们被纳入Spring容器进行管理。
- 配置管理:帮助管理应用中的Bean,确保它们能够被注入到需要它们的地方。
使用示例
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan(basePackages = "com.example.myapp")
public class AppConfig {
// 配置类
}
SSM常见注解
@Configuration
标记一个类为Spring的配置类。
@Configuration
public class AppConfig {
@Bean
public MyService myService() {
return new MyServiceImpl();
}
}
@Configuration
的作用
- 定义Bean:在配置类中,可以使用
@Bean
注解定义Spring管理的Bean。 - 替代XML配置:传统的Spring应用程序使用XML文件来配置Bean和其他设置,而使用
@Configuration
可以用Java代码替代这些XML配置,更加直观和方便。 - 集中配置:可以将相关的配置集中在一个或多个配置类中,方便管理和维护。
使用@Configuration
的例子
假设我们有一个简单的应用程序,需要配置一个服务类MyService
和一个存储库类MyRepository
。
定义Bean的配置类
@Configuration
public class AppConfig {
@Bean
public MyService myService() {
return new MyServiceImpl(myRepository());
}
@Bean
public MyRepository myRepository() {
return new MyRepositoryImpl();
}
}
在上面的例子中:
AppConfig
类被标记为@Configuration
,表明它是一个Spring的配置类。myService
和myRepository
方法被标记为@Bean
,表明它们返回的对象应该作为Spring的Bean管理。
@Service
在Spring框架中,@Service
注解用于标记一个类为服务层组件。它通常用于业务逻辑层,表示该类包含服务逻辑。@Service
注解的主要作用包括:
-
标识服务类:
@Service
明确表示这个类是服务层组件,便于代码的组织和维护。其他开发者可以一目了然地知道这个类是服务类。 -
组件扫描:
@Service
是Spring组件扫描的一部分。Spring在启动时会扫描带有@Service
注解的类,并将其注册为Spring应用上下文中的Bean。这意味着你可以在其他组件中通过依赖注入使用这个服务类。 -
业务逻辑层的职责:通过使用
@Service
注解,可以将业务逻辑与其他层(如控制器层、数据访问层)分离,遵循单一职责原则。
使用@Service
注解的例子
假设我们有一个简单的业务逻辑类,用于处理用户的相关操作:
定义服务类
import org.springframework.stereotype.Service;
@Service
public class UserService {
public void createUser(String username) {
// 业务逻辑,例如验证用户名,保存到数据库等
System.out.println("User " + username + " created.");
}
public void deleteUser(String username) {
// 业务逻辑,例如从数据库删除用户等
System.out.println("User " + username + " deleted.");
}
}
在这个例子中:
UserService
类被标记为@Service
,表明它是一个服务层组件。UserService
包含了创建和删除用户的业务逻辑。
使用服务类
我们可以在控制器类中注入并使用这个服务类:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/createUser")
@ResponseBody
public String createUser(@RequestParam String username) {
userService.createUser(username);
return "User " + username + " created.";
}
@GetMapping("/deleteUser")
@ResponseBody
public String deleteUser(@RequestParam String username) {
userService.deleteUser(username);
return "User " + username + " deleted.";
}
}
在这个例子中:
UserController
类被标记为@Controller
,表明它是一个控制器。UserService
通过@Autowired
注解自动注入到UserController
中。UserController
中定义了两个处理用户创建和删除的请求处理方法。
- @Configuration:标记一个类为Spring的配置类。
- @Component:泛指组件,Spring会自动检测并注册为Bean。
- @Service:标记一个类为服务层组件。
- @Repository:标记一个类为数据访问层组件。
- @Autowired:自动注入Bean,按类型装配。
- @Qualifier:与@Autowired一起使用,按名称装配Bean。
- @Resource:按名称或类型注入Bean。
- @Value:注入配置文件中的值。
- @Scope:指定Bean的作用域(singleton、prototype等)。
在Spring框架中,@Component
注解用于标记一个类为Spring的组件,使其成为Spring应用上下文中的Bean。@Component
是一个泛用的注解,它可以用于任何Spring管理的组件,而不局限于具体的层次(如服务层、数据访问层等)。
@Component
的作用
- 标识组件:
@Component
注解表示这个类是一个组件,可以被Spring容器管理和自动检测。 - 组件扫描:Spring在启动时会扫描带有
@Component
注解的类,并将其注册为Spring应用上下文中的Bean。这意味着你可以在其他组件中通过依赖注入使用这个类。 - 代码组
@Autowired
用一个更加通俗易懂的例子来解释@Autowired
的用法。假设我们要创建一个咖啡馆应用,其中有一个咖啡机和一个咖啡馆。咖啡馆需要依赖咖啡机来制作咖啡。
1. 定义咖啡机类
首先,我们定义一个咖啡机类,这个类包含制作咖啡的方法。
import org.springframework.stereotype.Component;
@Component // 这个注解告诉Spring这是一个组件,它会被Spring管理
public class CoffeeMachine {
public void makeCoffee() {
System.out.println("Coffee is being made.");
}
}
2. 定义咖啡馆类
接下来,我们定义一个咖啡馆类,这个类需要使用咖啡机来制作咖啡。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component // 这个注解告诉Spring这是一个组件,它会被Spring管理
public class CoffeeShop {
private CoffeeMachine coffeeMachine;
@Autowired // 这个注解告诉Spring自动将CoffeeMachine的实例注入到这里
public CoffeeShop(CoffeeMachine coffeeMachine) {
this.coffeeMachine = coffeeMachine;
}
public void serveCoffee() {
coffeeMachine.makeCoffee();
System.out.println("Serving coffee.");
}
}
3. 配置Spring应用上下文
我们需要一个配置类来告诉Spring扫描我们定义的组件。
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration // 标记这是一个配置类
@ComponentScan(basePackages = "com.example") // 告诉Spring扫描这个包中的组件
public class AppConfig {
}
4. 主类来运行应用
最后,我们创建一个主类来运行我们的应用。
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Main {
public static void main(String[] args) {
// 创建Spring应用上下文,并加载配置类
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
// 从Spring上下文中获取CoffeeShop的实例
CoffeeShop coffeeShop = context.getBean(CoffeeShop.class);
// 调用serveCoffee方法
coffeeShop.serveCoffee();
}
}
好处
-
解耦合:
- 通过依赖注入,类与类之间的依赖关系被Spring容器管理,从而减少了类之间的耦合度,使得代码更加灵活和易于维护。
-
简化代码:
- 自动注入减少了手动创建和管理依赖对象的代码,使代码更加简洁、清晰。
-
便于测试:
- 通过构造方法注入或setter方法注入,可以轻松地在单元测试中使用mock对象或替代实现,增强了代码的可测试性。
-
方便管理:
- Spring容器会自动管理Bean的生命周期,包括创建、初始化和销毁,这样开发者不需要手动管理这些过程。
-
配置灵活:
- 通过注解和配置类,可以轻松地配置和管理Bean,甚至可以在运行时根据条件决定注入哪个具体实现。
后续可以做的事情
-
扩展功能:
- 通过注入不同的实现类,可以很方便地扩展和修改功能。例如,你可以有多个
CoffeeMachine
的实现类,每个实现类制作不同类型的咖啡,然后在运行时根据配置选择具体的实现。
- 通过注入不同的实现类,可以很方便地扩展和修改功能。例如,你可以有多个
-
配置变更:
- 如果需要更改某个依赖的实现,只需修改配置或注入的Bean,而不需要修改业务逻辑代码。例如,从
CoffeeMachine
换成AdvancedCoffeeMachine
,只需要更改注入配置。
- 如果需要更改某个依赖的实现,只需修改配置或注入的Bean,而不需要修改业务逻辑代码。例如,从
-
轻松引入新功能:
- 例如,如果你想在制作咖啡前记录日志,只需要在现有的
CoffeeShop
类中添加一个日志记录组件,并通过@Autowired
注入,而不需要修改太多的现有代码。
- 例如,如果你想在制作咖啡前记录日志,只需要在现有的
Spring MVC的注解
SpringMVC是Spring框架的一部分,用于构建Web应用程序。它提供了一系列注解来简化开发。以下是SpringMVC中常见的注解及其用途,并附上简单的示例:
1. @Controller
标记一个类为SpringMVC的控制器,处理HTTP请求并返回视图名称。
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller
public class HomeController {
@RequestMapping(value = "/", method = RequestMethod.GET)
public String home() {
return "home"; // 返回视图名称
}
}
2. @RequestMapping
映射URL到控制器方法,可以用于类和方法上。可以指定路径、请求方法、参数等。
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller
@RequestMapping("/user")
public class UserController {
@RequestMapping(value = "/profile", method = RequestMethod.GET)
public String userProfile() {
return "userProfile"; // 返回视图名称
}
}
3. @GetMapping
简化了@RequestMapping
,用于处理GET请求。
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class ProductController {
@GetMapping("/products")
public String listProducts() {
return "productList"; // 返回视图名称
}
}
4. @PostMapping
简化了@RequestMapping
,用于处理POST请求。
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
public class FormController {
@PostMapping("/submitForm")
public String submitForm(@RequestParam("name") String name) {
// 处理表单提交
return "formResult"; // 返回视图名称
}
}
5. @RequestParam
用于将请求参数绑定到控制器方法的参数上。
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
public class SearchController {
@GetMapping("/search")
public String search(@RequestParam("query") String query) {
// 处理搜索请求
return "searchResult"; // 返回视图名称
}
}
Spring Boot 是基于 Spring 框架的一种简化配置方式,它提供了一系列注解,用于简化配置和开发。以下是 Spring Boot 常见的注解及其用途,并附带简单的示例代码:
1. @SpringBootApplication
这是一个组合注解,包括 @Configuration
、@EnableAutoConfiguration
和 @ComponentScan
。它标记一个主类,用于启动 Spring Boot 应用。
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
2. @EnableAutoConfiguration
启用 Spring Boot 的自动配置机制。
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableAutoConfiguration
public class MyConfiguration {
}
3. @ComponentScan
扫描指定包及其子包中的组件(例如 @Component、@Service、@Repository 等)。
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan(basePackages = "com.example.myapp")
public class MyConfiguration {
}
4. @ConfigurationProperties
绑定外部配置到一个 Java Bean。常用于将配置文件中的属性映射到类中。
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "app")
public class AppProperties {
private String name;
private String description;
// getters and setters
}
Mybatis
Mybatis执行流程
假设我们有一个简单的用户管理系统,包含一个 User
类和一个 UserMapper
接口,用于执行对用户表的CRUD(创建、读取、更新、删除)操作。我们需要通过 MyBatis 实现一个查询用户信息的功能。
1. 配置文件
首先,我们需要一个 MyBatis 的配置文件 mybatis-config.xml
和一个映射文件 UserMapper.xml
。
<!-- mybatis-config.xml -->
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mydb"/>
<property name="username" value="root"/>
<property name="password" value="password"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="UserMapper.xml"/>
</mappers>
</configuration>
2. 映射接口
定义一个 UserMapper
接口,用于声明数据库操作方法。
// UserMapper.java
package com.example;
import org.apache.ibatis.annotations.Select;
public interface UserMapper {
User getUserById(int id);
}
3. 实体类
定义一个 User
实体类,用于映射数据库表中的记录。
// User.java
package com.example;
public class User {
private int id;
private String name;
private int age;
// getters and setters
}
4. 执行流程
现在,我们来看一下 MyBatis 执行查询操作的流程:
4.1 加载配置文件
首先,MyBatis 会加载配置文件 mybatis-config.xml
。这一步可以理解为我们需要先读取一份操作指南,知道该如何配置和连接数据库。
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml"); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
4.2 获取 SqlSession
从 SqlSessionFactory
中获取 SqlSession
。可以理解为我们需要一位帮手 SqlSession
,通过他来执行具体的数据库操作。
try (SqlSession session = sqlSessionFactory.openSession())
{ UserMapper mapper = session.getMapper(UserMapper.class);
User user = mapper.getUserById(1);
System.out.println(user.getName());
}
4.3 映射方法执行
通过 SqlSession
获取 UserMapper
的实例,并调用 getUserById
方法。这一步相当于我们告诉帮手 SqlSession
,我们想要查找 ID 为1的用户信息。
4.4 执行 SQL 语句
MyBatis 会根据 UserMapper.xml
中的配置,生成并执行对应的 SQL 语句:
SELECT * FROM users WHERE id = 1;
这一步相当于我们的帮手 SqlSession
按照我们的指示去数据库中查找 ID 为1的用户。
4.5 映射结果
执行 SQL 语句后,MyBatis 会将查询结果映射到 User
实体类中。这一步可以理解为帮手 SqlSession
将数据库中查找到的用户信息填充到 User
对象中,并返回给我们。
4.6 关闭 SqlSession
最后,关闭 SqlSession
。这是一个清理工作的步骤,确保资源被正确释放。
总结
- 加载配置文件:读取 MyBatis 配置文件,初始化 MyBatis 环境。
- 获取 SqlSession:通过
SqlSessionFactory
获取一个SqlSession
实例,类似于创建一个帮手。 - 执行映射方法:调用映射接口的方法,执行数据库操作。
- 生成并执行 SQL:MyBatis 根据映射文件生成并执行 SQL 语句。
- 映射结果:将 SQL 执行结果映射到对应的实体类。
- 关闭 SqlSession:关闭
SqlSession
,释放资源。
延迟加载(Lazy Loading)是一种设计模式,当访问对象的某些属性或关联对象时才真正加载这些数据,而不是在初始访问对象时就加载所有数据。这样可以提高应用程序的性能,减少不必要的数据加载,节省内存和带宽。
延迟加载的概念
在许多应用中,特别是涉及到数据库访问时,数据量可能很大且复杂。如果在每次访问时都加载所有相关数据,可能会导致性能问题和资源浪费。因此,延迟加载技术允许在需要的时候才加载相关数据。
例如,一个“订单”对象可能有一个“客户”对象以及多个“订单项”对象。使用延迟加载技术,当你访问“订单”对象时,可能只加载订单的基本信息,而不加载“客户”对象和“订单项”对象。当你真正需要访问“客户”对象或“订单项”对象时,才去加载这些数据。
MyBatis 支持延迟加载
在代码中使用 MyBatis 进行查询,当你访问延迟加载的属性时,才会触发相应的数据加载:
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import java.io.InputStream;
import org.apache.ibatis.io.Resources;
public class MyBatisExample {
public static void main(String[] args) throws Exception {
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
try (SqlSession session = sqlSessionFactory.openSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
User user = mapper.getUserById(1);
// 此时不会加载 address
System.out.println("User Name: " + user.getName());
// 当访问 address 属性时,才会触发延迟加载
System.out.println("User Address: " + user.getAddress().getFullAddress());
}
}
}
总结
- 延迟加载 是一种在需要时才加载数据的技术,避免了不必要的数据加载,提升了应用的性能。
- MyBatis 支持延迟加载,通过配置文件设置
lazyLoadingEnabled
和aggressiveLazyLoading
属性,以及在映射文件中使用association
元素实现。 - 使用 MyBatis 的延迟加载功能,可以减少数据库查询次数,优化数据访问性能。
MyBatis 提供了两级缓存机制:一级缓存(本地缓存)和二级缓存(全局缓存),用于提升数据库访问的性能。下面通过通俗的例子来解释这两种缓存。
一级缓存
一级缓存是 SqlSession 级别的缓存,它在同一个 SqlSession 中有效。简单来说,就是在同一个数据库会话期间,MyBatis 会缓存查询结果。如果在同一个会话期间再次查询相同的数据,MyBatis 会直接从缓存中返回结果,而不需要再次访问数据库。
示例场景:
假设我们有一个图书管理系统,我们要查询一本书的信息。
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import java.io.InputStream;
import org.apache.ibatis.io.Resources;
public class MyBatisExample {
public static void main(String[] args) throws Exception {
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
try (SqlSession session = sqlSessionFactory.openSession()) {
BookMapper mapper = session.getMapper(BookMapper.class);
// 第一次查询
Book book1 = mapper.getBookById(1);
System.out.println("First query: " + book1.getTitle());
// 第二次查询,同一个 SqlSession,会使用一级缓存
Book book2 = mapper.getBookById(1);
System.out.println("Second query: " + book2.getTitle());
}
}
}
在这个例子中,BookMapper
接口有一个方法 getBookById(int id)
,用于查询图书信息。第一次查询后,结果会被缓存到 SqlSession 的一级缓存中。第二次查询相同的图书信息时,MyBatis 会直接从一级缓存中返回结果,而不需要再次访问数据库。
二级缓存
二级缓存是 Mapper 级别的缓存,它在多个 SqlSession 中共享。也就是说,不同的数据库会话可以共享同一个缓存。这种缓存可以跨多个数据库会话,因此适用于需要频繁访问的公共数据。
示例场景:
继续图书管理系统的例子,这次我们在不同的 SqlSession 中查询图书信息。
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import java.io.InputStream;
import org.apache.ibatis.io.Resources;
public class MyBatisExample {
public static void main(String[] args) throws Exception {
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 第一个 SqlSession
try (SqlSession session1 = sqlSessionFactory.openSession()) {
BookMapper mapper = session1.getMapper(BookMapper.class);
// 第一次查询
Book book1 = mapper.getBookById(1);
System.out.println("First query: " + book1.getTitle());
}
// 第二个 SqlSession
try (SqlSession session2 = sqlSessionFactory.openSession()) {
BookMapper mapper = session2.getMapper(BookMapper.class);
// 第二次查询,会使用二级缓存
Book book2 = mapper.getBookById(1);
System.out.println("Second query: " + book2.getTitle());
}
}
}
在这个例子中,第一次查询会将结果缓存到二级缓存中。当第二次查询相同的图书信息时,如果缓存没有失效,MyBatis 会直接从二级缓存中返回结果,而不需要再次访问数据库。
总结
- 一级缓存:SqlSession 级别的缓存,在同一个 SqlSession 中有效。它用于缓存相同会话中的查询结果,减少数据库访问次数。
- 二级缓存:Mapper 级别的缓存,在多个 SqlSession 中共享。它用于缓存多个会话中的查询结果,适用于频繁访问的公共数据。
通过使用这两种缓存机制,MyBatis 可以显著提高数据库访问的性能,减少不必要的数据库查询,从而提升应用程序的效率。
SqlSession
是 MyBatis 框架中最核心的接口之一,代表了与数据库交互的一个会话(session)。通过 SqlSession
,我们可以执行 SQL 语句,获取映射器接口的实现,管理事务等。它类似于 JDBC 中的 Connection
对象,但提供了更高级和简化的 API。
主要功能
- 执行 SQL 语句:包括增、删、改、查操作。
- 获取映射器:通过映射器接口(Mapper Interface)来执行数据库操作。
- 管理事务:包括事务的提交和回滚。
- 缓存管理:包括一级缓存和二级缓存的管理。
具体用途
1. 执行 SQL 语句
SqlSession
提供了多种方法来执行 SQL 语句,包括 selectOne
、selectList
、insert
、update
和 delete
等。例如:
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
public class MyBatisExample {
public static void main(String[] args) {
SqlSessionFactory sqlSessionFactory = ... // 初始化 SqlSessionFactory
try (SqlSession session = sqlSessionFactory.openSession()) {
// 执行查询
User user = session.selectOne("com.example.UserMapper.selectUser", 1);
System.out.println(user.getName());
// 执行插入
User newUser = new User();
newUser.setName("John Doe");
session.insert("com.example.UserMapper.insertUser", newUser);
// 提交事务
session.commit();
}
}
}
配置SSM框架
1、搭建数据库环境
CREATE DATABASE `ssmbuild`;
USE `ssmbuild`;
DROP TABLE IF EXISTS `books`;
CREATE TABLE `books` (
`bookID` INT(10) NOT NULL AUTO_INCREMENT COMMENT '书id',
`bookName` VARCHAR(100) NOT NULL COMMENT '书名',
`bookCounts` INT(11) NOT NULL COMMENT '数量',
`detail` VARCHAR(200) NOT NULL COMMENT '描述',
KEY `bookID` (`bookID`)
) ENGINE=INNODB DEFAULT CHARSET=utf8
INSERT INTO `books`(`bookID`,`bookName`,`bookCounts`,`detail`)VALUES
(1,'Java',1,'XXXXX1'),
(2,'MySQL',10,'XXXXX2'),
(3,'Linux',5,'XXXXX3');
2、新建Maven项目,导入相关的pom依赖
<dependencies>
<!--Junit-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<!--数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<!-- 数据库连接池 -->
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.2</version>
</dependency>
<!--Servlet - JSP -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.2</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<!--Mybatis-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.2</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.2</version>
</dependency>
<!--Spring-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.1.9.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.1.9.RELEASE</version>
</dependency>
</dependencies>
设置Maven资源过滤
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>
3、建立基本结构和框架
① com.XXXX.pojo
② comXXXX.dao
③ com.XXXX.service
④ com.XXXX.controller
⑤mybatis-config.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>
</configuration>
⑥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">
</beans>
3、编写Myvatis层
①数据库配置文件 database.properties
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/ssmbuild?useSSL=true&useUnicode=true&characterEncoding=utf8
jdbc.username=root
jdbc.password=123456
②编写MyBatis的核心配置文件
<?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>
<typeAliases>
<package name="com.XXX.pojo"/>
</typeAliases>
<mappers>
<mapper resource="com/XXX/dao/BookMapper.xml"/>
</mappers>
</configuration>
③编写数据库对应的实体类 com.XXXXX.pojo.Books
package com.kuang.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Books {
private int bookID;
private String bookName;
private int bookCounts;
private String detail;
}
④编写Dao层的 Mapper接口
package com.XXX.dao;
import com.XXX.pojo.Books;
import java.util.List;
public interface BookMapper {
//增加一个Book
int addBook(Books book);
//根据id删除一个Book
int deleteBookById(int id);
//更新Book
int updateBook(Books books);
//根据id查询,返回一个Book
Books queryBookById(int id);
//查询全部Book,返回list集合
List<Books> queryAllBook();
}
⑤编写接口对应的 Mapper.xml 文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.kuang.dao.BookMapper">
<!--增加一个Book-->
<insert id="addBook" parameterType="Books">
insert into ssmbuild.books(bookName,bookCounts,detail)
values (#{bookName}, #{bookCounts}, #{detail})
</insert>
<!--根据id删除一个Book-->
<delete id="deleteBookById" parameterType="int">
delete from ssmbuild.books where bookID=#{bookID}
</delete>
<!--更新Book-->
<update id="updateBook" parameterType="Books">
update ssmbuild.books
set bookName = #{bookName},bookCounts = #{bookCounts},detail = #{detail}
where bookID = #{bookID}
</update>
<!--根据id查询,返回一个Book-->
<select id="queryBookById" resultType="Books">
select * from ssmbuild.books
where bookID = #{bookID}
</select>
<!--查询全部Book-->
<select id="queryAllBook" resultType="Books">
SELECT * from ssmbuild.books
</select>
</mapper>
4、编写Service层
①接口
package com.XXX.service;
import com.kuang.pojo.Books;
import java.util.List;
//BookService:底下需要去实现,调用dao层
public interface BookService {
//增加一个Book
int addBook(Books book);
//根据id删除一个Book
int deleteBookById(int id);
//更新Book
int updateBook(Books books);
//根据id查询,返回一个Book
Books queryBookById(int id);
//查询全部Book,返回list集合
List<Books> queryAllBook();
}
②实现类
package com.kuang.service;
import com.kuang.dao.BookMapper;
import com.kuang.pojo.Books;
import java.util.List;
public class BookServiceImpl implements BookService {
//调用dao层的操作,设置一个set接口,方便Spring管理
private BookMapper bookMapper;
public void setBookMapper(BookMapper bookMapper) {
this.bookMapper = bookMapper;
}
public int addBook(Books book) {
return bookMapper.addBook(book);
}
public int deleteBookById(int id) {
return bookMapper.deleteBookById(id);
}
public int updateBook(Books books) {
return bookMapper.updateBook(books);
}
public Books queryBookById(int id) {
return bookMapper.queryBookById(id);
}
public List<Books> queryAllBook() {
return bookMapper.queryAllBook();
}
}
5、Dao层
①spring-dao.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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<!-- 配置整合mybatis -->
<!-- 1.关联数据库文件 -->
<context:property-placeholder location="classpath:database.properties"/>
<!-- 2.数据库连接池 -->
<!--数据库连接池
dbcp 半自动化操作 不能自动连接
c3p0 自动化操作(自动的加载配置文件 并且设置到对象里面)
-->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<!-- 配置连接池属性 -->
<property name="driverClass" value="${jdbc.driver}"/>
<property name="jdbcUrl" value="${jdbc.url}"/>
<property name="user" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
<!-- c3p0连接池的私有属性 -->
<property name="maxPoolSize" value="30"/>
<property name="minPoolSize" value="10"/>
<!-- 关闭连接后不自动commit -->
<property name="autoCommitOnClose" value="false"/>
<!-- 获取连接超时时间 -->
<property name="checkoutTimeout" value="10000"/>
<!-- 当获取连接失败重试次数 -->
<property name="acquireRetryAttempts" value="2"/>
</bean>
<!-- 3.配置SqlSessionFactory对象 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!-- 注入数据库连接池 -->
<property name="dataSource" ref="dataSource"/>
<!-- 配置MyBaties全局配置文件:mybatis-config.xml -->
<property name="configLocation" value="classpath:mybatis-config.xml"/>
</bean>
<!-- 4.配置扫描Dao接口包,动态实现Dao接口注入到spring容器中 -->
<!--解释 : https://www.cnblogs.com/jpfss/p/7799806.html-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!-- 注入sqlSessionFactory -->
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
<!-- 给出需要扫描Dao接口包 -->
<property name="basePackage" value="com.kuang.dao"/>
</bean>
</beans>
6、Spring整合service层
<?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">
<!-- 扫描service相关的bean -->
<context:component-scan base-package="com.kuang.service" />
<!--BookServiceImpl注入到IOC容器中-->
<bean id="BookServiceImpl" class="com.kuang.service.BookServiceImpl">
<property name="bookMapper" ref="bookMapper"/>
</bean>
<!-- 配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 注入数据库连接池 -->
<property name="dataSource" ref="dataSource" />
</bean>
</beans>
7、SpringMVC层
①web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<!--DispatcherServlet-->
<servlet>
<servlet-name>DispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<!--一定要注意:我们这里加载的是总的配置文件,之前被这里坑了!-->
<param-value>classpath:applicationContext.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>DispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!--encodingFilter-->
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>
org.springframework.web.filter.CharacterEncodingFilter
</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>utf-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!--Session过期时间-->
<session-config>
<session-timeout>15</session-timeout>
</session-config>
</web-app>
②spring-mvc.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:mvc="http://www.springframework.org/schema/mvc"
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/mvc
https://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!-- 配置SpringMVC -->
<!-- 1.开启SpringMVC注解驱动 -->
<mvc:annotation-driven />
<!-- 2.静态资源默认servlet配置-->
<mvc:default-servlet-handler/>
<!-- 3.配置jsp 显示ViewResolver视图解析器 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.JstlView" />
<property name="prefix" value="/WEB-INF/jsp/" />
<property name="suffix" value=".jsp" />
</bean>
<!-- 4.扫描web相关的bean -->
<context:component-scan base-package="com.kuang.controller" />
</beans>
③Spring配置整合文件,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">
<import resource="spring-dao.xml"/>
<import resource="spring-service.xml"/>
<import resource="spring-mvc.xml"/>
</beans>
8、增删改查。。。。