一、Spring基础入门
1.1 什么是Spring框架
Spring诞生于2003年,是由Rod Johnson开发并开源的Java开发框架。其设计初衷是为了解决企业级应用开发中复杂的依赖管理和业务逻辑分离问题。它提供了全面的编程和配置模型,用于构建企业级应用程序。Spring的核心是控制反转(IOC)和面向切面编程(AOP),这两个特性帮助开发者解耦应用组件,提高代码的可维护性和可测试性。
在早期的Java企业级开发中,开发者往往需要编写大量繁琐的样板代码来管理对象之间的依赖关系,并且横切关注点(如日志记录、事务管理)与业务逻辑紧密耦合,使得代码的维护和扩展变得极为困难。Spring的出现,极大地改变了这种局面。通过IOC容器,Spring能够自动管理对象的创建、配置和生命周期,让开发者无需手动处理这些复杂的依赖关系。而AOP则允许将横切关注点从业务逻辑中分离出来,以一种优雅的方式实现系统级功能的统一管理。
1.2 Spring的核心特性(控制反转IOC、依赖注入DI)
控制反转(IOC):是一种设计思想,将对象的创建和管理从应用程序代码转移到Spring容器中。例如,传统方式中对象A需要自己创建并管理依赖对象B,而在IOC中,由Spring容器负责创建和管理B,并将其提供给A。这种方式使得对象之间的依赖关系更加清晰,也方便了代码的测试和维护。比如在一个电商系统中,订单服务(OrderService)依赖于商品服务(ProductService)来获取商品信息。在传统模式下,OrderService需要在内部自行创建ProductService的实例,这就导致了OrderService与ProductService之间的强耦合。而使用Spring的IOC,我们可以将ProductService的创建和管理交给Spring容器,OrderService只需要声明对ProductService的依赖,Spring容器会在运行时自动将ProductService注入到OrderService中。
依赖注入(DI):是IOC的具体实现方式,Spring容器通过构造函数、Setter方法或字段注入将依赖对象注入到目标对象中。以构造函数注入为例,假设我们有一个UserService类,它依赖于UserRepository来进行用户数据的持久化操作。
package com.example;
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
// 业务方法
}
在Spring的配置文件中,我们可以这样配置:
<bean id="userRepository" class="com.example.UserRepository">
<!-- 配置UserRepository的属性 -->
</bean>
<bean id="userService" class="com.example.UserService">
<constructor-arg ref="userRepository"/>
</bean>
这样,Spring容器在创建UserService实例时,会将已经创建好的UserRepository实例通过构造函数注入进去。
1.3 搭建Spring开发环境(Maven配置)
- 安装Maven:从Maven官网(maven.apache.org/download.cg… -> “高级” -> “环境变量”,在“系统变量”中新建
MAVEN_HOME,值为Maven的解压目录,然后在PATH变量中添加%MAVEN_HOME%\bin。在Linux或Mac系统中,可以编辑~/.bashrc或~/.zshrc文件,添加export MAVEN_HOME=/path/to/maven和export PATH=$MAVEN_HOME/bin:$PATH,然后执行source ~/.bashrc或source ~/.zshrc使配置生效。 - 创建Maven项目:在命令行中执行
mvn archetype:generate -DgroupId=com.example -DartifactId=spring -Dversion=1.0.0 -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false,创建一个基础Maven项目。这个命令会根据Maven的maven-archetype-quickstart原型模板创建一个项目,其中groupId表示项目的组ID,类似于Java的包名,用于标识项目的所属组织或模块;artifactId是项目的唯一标识符,通常是项目的名称;version是项目的版本号。 - 配置
pom.xml文件:添加Spring依赖。
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.10</version>
</dependency>
</dependencies>
这里添加的spring-context依赖包含了Spring IOC容器的核心功能,是使用Spring框架的基础。
4. 设置Java版本为17:在pom.xml中添加如下配置:
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
这样可以确保Maven在编译项目时使用Java 17版本,以充分利用Java 17的新特性和性能优化。
1.4 第一个Spring程序(Hello World示例)
- 创建一个Java类
HelloWorld:
package com.example;
public class HelloWorld {
private String message;
public void setMessage(String message) {
this.message = message;
}
public void printMessage() {
System.out.println("Hello World! Message: " + message);
}
}
这个类非常简单,它有一个message属性和两个方法,setMessage用于设置消息内容,printMessage用于打印消息。
2. 创建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">
<bean id="helloWorld" class="com.example.HelloWorld">
<property name="message" value="Hello, Spring!"/>
</bean>
</beans>
在这个配置文件中,我们定义了一个bean,它的id为helloWorld,对应的类是com.example.HelloWorld,并通过<property>标签为message属性赋值。
3. 创建测试类App:
package com.example;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class App {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
HelloWorld helloWorld = (HelloWorld) context.getBean("helloWorld");
helloWorld.printMessage();
}
}
在App类中,我们首先通过ClassPathXmlApplicationContext加载applicationContext.xml配置文件,创建Spring上下文。然后从上下文中获取helloWorld这个bean,并调用其printMessage方法。运行App类,控制台将输出Hello World! Message: Hello, Spring!。
二、Spring核心机制深入
2.1 IOC容器详解(Bean的创建、生命周期管理)
Bean的创建:Spring容器通过读取配置文件(XML或注解配置)来创建Bean。以之前的HelloWorld类为例,在applicationContext.xml中配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema - instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring - beans.xsd">
<bean id="helloWorld" class="com.example.HelloWorld">
<property name="message" value="Hello, Spring!"/>
</bean>
</beans>
当Spring容器启动时,它会解析这个配置文件。首先,根据<bean>标签的class属性,Spring使用反射机制,通过Class.forName()方法加载com.example.HelloWorld类的字节码。然后,利用Constructor对象调用类的构造函数来创建HelloWorld类的实例。在创建实例后,Spring会根据<property>标签,通过反射调用HelloWorld类的setMessage方法,将Hello, Spring!这个值注入到message属性中。
如果使用注解配置,假设项目结构如下:
package com.example;
import org.springframework.stereotype.Component;
@Component
public class HelloWorld {
private String message;
public void setMessage(String message) {
this.message = message;
}
public void printMessage() {
System.out.println("Hello World! Message: " + message);
}
}
同时,在Spring配置类中开启组件扫描:
package com.example;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan("com.example")
public class AppConfig {
}
Spring容器启动时,会扫描com.example包及其子包下带有@Component注解的类。当扫描到HelloWorld类时,同样会使用反射机制创建其实例,并在后续根据依赖关系进行属性注入等操作。
生命周期管理:Bean的生命周期包括实例化、属性赋值、初始化、使用和销毁。Spring提供了多种方式来自定义初始化和销毁逻辑。
实现接口方式:可以通过实现InitializingBean接口的afterPropertiesSet方法和DisposableBean接口的destroy方法来自定义初始化和销毁逻辑。例如:
package com.example;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;
@Component
public class LifeCycleBean implements InitializingBean, DisposableBean {
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("LifeCycleBean初始化逻辑");
}
@Override
public void destroy() throws Exception {
System.out.println("LifeCycleBean销毁逻辑");
}
}
当Spring容器创建LifeCycleBean实例并完成属性赋值后,会调用afterPropertiesSet方法执行初始化逻辑;当容器关闭时,会调用destroy方法执行销毁逻辑。
使用注解方式:使用@PostConstruct注解标记初始化方法,@PreDestroy注解标记销毁方法。示例如下:
package com.example;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import org.springframework.stereotype.Component;
@Component
public class AnnotationLifeCycleBean {
@PostConstruct
public void init() {
System.out.println("AnnotationLifeCycleBean初始化逻辑");
}
@PreDestroy
public void destroy() {
System.out.println("AnnotationLifeCycleBean销毁逻辑");
}
}
这种方式更加简洁,也无需实现特定接口,@PostConstruct注解的方法会在Bean创建并完成属性赋值后执行,@PreDestroy注解的方法会在容器销毁Bean前执行。
此外,还可以在applicationContext.xml中通过<bean>标签的init - method和destroy - method属性指定初始化和销毁方法,如下所示:
<bean id="customLifeCycleBean" class="com.example.CustomLifeCycleBean"
init - method="customInit" destroy - method="customDestroy">
</bean>
对应的CustomLifeCycleBean类中定义customInit和customDestroy方法:
package com.example;
public class CustomLifeCycleBean {
public void customInit() {
System.out.println("CustomLifeCycleBean自定义初始化逻辑");
}
public void customDestroy() {
System.out.println("CustomLifeCycleBean自定义销毁逻辑");
}
}
通过这些方式,可以全面地控制Bean在Spring IOC容器中的生命周期,满足不同的业务需求 。
2.2 DI的多种方式(构造器注入、Setter注入)
构造器注入:在pom.xml中添加一个新的依赖类MessageService:
package com.example;
public class MessageService {
private String message;
public MessageService(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}
在applicationContext.xml中配置构造器注入:
<bean id="messageService" class="com.example.MessageService">
<constructor - arg value="Constructor - injected message"/>
</bean>
构造器注入的好处是可以确保依赖对象在创建时就已经被注入,对象在使用时不需要再担心依赖对象为空的情况。而且通过构造器注入,依赖关系一目了然,便于理解和维护。
Setter注入:如前面HelloWorld类中通过setMessage方法实现Setter注入。Setter注入相对更加灵活,它允许在对象创建后再设置依赖关系。例如,在一些情况下,我们可能需要根据不同的环境或配置来动态地设置依赖对象,这时Setter注入就非常合适。而且Setter注入也符合JavaBean的设计模式,便于代码的可读性和维护性。
2.3 Bean的作用域与自动装配
作用域:Spring提供了多种Bean作用域,如singleton(单例,默认)、prototype(原型,每次请求创建新实例)等。在applicationContext.xml中可以如下配置:
<bean id="prototypeBean" class="com.example.HelloWorld" scope="prototype">
<property name="message" value="Prototype - scoped bean"/>
</bean>
singleton作用域的Bean在Spring容器中只会存在一个实例,所有对该Bean的引用都指向同一个实例。这种作用域适用于无状态的Bean,如工具类、服务类等,它们不需要维护自身的状态,多个地方使用同一个实例可以节省内存开销。而prototype作用域的Bean每次被请求时都会创建一个新的实例,适用于有状态的Bean,如保存用户会话信息的Bean,每个用户的会话信息都需要独立保存,所以需要为每个请求创建新的实例。
自动装配:通过@Autowired注解实现自动装配。首先在pom.xml中添加依赖以支持注解配置:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring - context - support</artifactId>
<version>6.0.10</version>
</dependency>
然后在需要自动装配的类中使用@Autowired:
package com.example;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class AutoWiredClass {
private MessageService messageService;
@Autowired
public AutoWiredClass(MessageService messageService) {
this.messageService = messageService;
}
public void printMessage() {
System.out.println("Auto - wired message: " + messageService.getMessage());
}
}
并在配置文件applicationContext.xml中开启组件扫描:
<context:component - scan base - package="com.example"/>
自动装配使得Spring容器能够自动识别并注入依赖对象,大大减少了配置的工作量。@Autowired注解可以用于构造函数、Setter方法和字段上,根据不同的使用场景选择合适的方式进行依赖注入。在使用自动装配时,Spring会根据类型来匹配需要注入的Bean,如果存在多个相同类型的Bean,还可以结合@Qualifier注解来指定具体要注入的Bean。
三、Spring AOP编程
3.1 AOP概念与原理
AOP(面向切面编程)是一种编程范式,它允许开发者将横切关注点(如日志记录、事务管理)与业务逻辑分离。Spring AOP通过代理模式实现,在运行时动态地将切面逻辑织入到目标方法中。
在传统的面向对象编程中,业务逻辑通常被封装在各个对象的方法中,而一些横切关注点,如日志记录、事务管理、权限控制等,会在多个方法中重复出现。这就导致了代码的冗余和维护的困难。AOP的出现就是为了解决这个问题,它将这些横切关注点抽象成切面,通过代理机制在运行时将切面逻辑织入到目标方法的执行过程中。
Spring AOP主要基于动态代理实现,当目标对象实现了接口时,Spring会使用JDK动态代理;当目标对象没有实现接口时,Spring会使用CGLIB代理。代理对象会在目标方法执行前后插入切面逻辑,从而实现横切关注点的统一管理。
3.2 切入点、通知、切面的理解与使用
切入点:定义了哪些方法需要被织入切面逻辑。例如,使用AspectJ表达式定义切入点,execution(* com.example.*.*(..))表示匹配com.example包及其子包下的所有方法:
@Pointcut("execution(* com.example.*.*(..))")
public void allMethods() {}
AspectJ表达式是一种强大的切入点描述语言,它可以根据方法签名、类名、包名等多种条件来精确匹配需要织入切面的方法。除了execution表达式外,还有within、this、target等表达式。within用于匹配指定类型内的方法,比如within(com.example.service.*)表示匹配com.example.service包下所有类的方法;this表示匹配当前代理对象类型的方法,target则匹配目标对象类型的方法 。
通知:定义了在切入点处执行的具体逻辑,不同类型的通知有不同的执行时机。
- 前置通知(@Before):在目标方法执行之前执行。例如,记录方法开始执行的日志:
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LoggingAspect {
@Pointcut("execution(* com.example.*.*(..))")
public void allMethods() {}
@Before("allMethods()")
public void beforeAdvice() {
System.out.println("方法开始执行,前置通知");
}
}
- 后置通知(@After):无论目标方法执行是否成功,在方法执行之后都会执行。比如记录方法执行结束的日志:
@After("allMethods()")
public void afterAdvice() {
System.out.println("方法执行结束,后置通知");
}
- 返回后通知(@AfterReturning):在目标方法成功执行并返回结果之后执行。可以获取目标方法的返回值进行处理,例如对返回值进行日志记录:
@AfterReturning(pointcut = "allMethods()", returning = "result")
public void afterReturningAdvice(Object result) {
System.out.println("方法成功执行并返回结果,返回后通知,返回值为: " + result);
}
- 异常通知(@AfterThrowing):当目标方法抛出异常时执行。可以用于记录异常信息,方便后续排查问题:
@AfterThrowing(pointcut = "allMethods()", throwing = "ex")
public void afterThrowingAdvice(Exception ex) {
System.out.println("方法执行抛出异常,异常通知,异常信息为: " + ex.getMessage());
}
- 环绕通知(@Around):环绕目标方法执行,可以在方法执行前后自定义逻辑,并且可以控制目标方法是否执行以及何时执行。例如,统计方法执行时间:
@Around("allMethods()")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
Object result = joinPoint.proceed();
long endTime = System.currentTimeMillis();
System.out.println("方法执行时间为: " + (endTime - startTime) + " 毫秒");
return result;
}
切面:将切入点和通知组合在一起,形成一个完整的横切逻辑。上述代码中的LoggingAspect类就是一个切面,它定义了切入点allMethods以及各种通知。
在pom.xml中添加AOP依赖:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring - aspectj</artifactId>
<version>6.0.10</version>
</dependency>
并在Spring的配置文件applicationContext.xml中开启AspectJ自动代理,配置如下:
<?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">
<!-- 其他bean配置 -->
<!-- 开启AspectJ自动代理 -->
<aop:aspectj - autoproxy/>
</beans>
在这个applicationContext.xml文件中,首先通过xmlns:aop="http://www.springframework.org/schema/aop"引入了AOP命名空间,然后通过<aop:aspectj - autoproxy/>标签开启AspectJ自动代理,这样Spring容器在创建Bean时,会自动为匹配切入点的Bean创建代理对象,从而将切面逻辑织入到目标方法中。
3.3 AOP在日志记录、事务管理等场景的应用
- 日志记录:通过AOP可以在方法执行前后记录日志,方便追踪和调试。如上述
LoggingAspect类中,通过不同的通知实现了对方法执行过程的全面日志记录。在一个大型项目中,可能有众多的业务方法,使用AOP进行日志记录可以避免在每个业务方法中重复编写日志代码,提高代码的可维护性和可读性。例如在一个电商系统中,对商品查询、订单处理等业务方法进行日志记录,能清晰了解系统运行状况。 - 事务管理:使用
@Transactional注解结合AOP实现事务的自动管理,确保数据库操作的原子性。在pom.xml中添加事务管理依赖:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring - tx</artifactId>
<version>6.0.10</version>
</dependency>
在配置文件applicationContext.xml中配置事务管理器:
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<tx:annotation - driven transaction - manager="transactionManager"/>
然后在需要事务管理的方法或类上添加@Transactional注解。当一个方法被@Transactional注解标记后,Spring AOP会自动为该方法创建事务切面,在方法执行前开启事务,方法执行成功后提交事务,若方法执行过程中抛出异常则回滚事务。比如在银行转账操作中,涉及到转出账户金额减少和转入账户金额增加两个数据库操作,使用事务管理能保证这两个操作要么都成功,要么都失败 。
四、Spring与数据库交互
在企业级应用开发中,与数据库进行交互是非常核心的部分。Spring提供了多种方式来简化数据库操作,包括JDBC模板、整合第三方持久化框架(如MyBatis)以及强大的事务管理机制,使得开发者能够高效、可靠地处理数据库相关业务。
4.1 JDBC模板的使用
JDBC(Java Database Connectivity)是Java访问数据库的标准接口,而Spring的JDBC模板则是对原生JDBC API的进一步封装,极大地简化了数据库操作,减少了重复代码,提高了开发效率和代码的可维护性。
首先,在pom.xml中添加JDBC和数据库驱动依赖,以MySQL为例:
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring - jdbc</artifactId>
<version>6.0.10</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql - connector - java</artifactId>
<version>8.0.33</version>
</dependency>
</dependencies>
这两个依赖分别引入了Spring的JDBC模块和MySQL数据库的驱动,确保项目能够使用Spring的JDBC功能并连接到MySQL数据库。
接着,配置数据源和JDBC模板:
<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/yourdb"/>
<property name="username" value="root"/>
<property name="password" value="password"/>
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
这里使用org.apache.commons.dbcp2.BasicDataSource来配置数据源,设置了数据库驱动类、连接URL、用户名和密码。jdbcTemplate则依赖于这个数据源,用于执行各种数据库操作。
4.1.1 查询操作
使用JDBC模板进行查询操作时,queryForList方法可以方便地返回结果集的列表形式。例如,查询用户表中的所有用户:
package com.example;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Map;
@Repository
public class UserDao {
@Autowired
private JdbcTemplate jdbcTemplate;
public List<Map<String, Object>> getUsers() {
String sql = "SELECT * FROM users";
return jdbcTemplate.queryForList(sql);
}
}
在实际应用中,可能需要根据条件查询用户。比如根据用户ID查询单个用户:
public Map<String, Object> getUserById(int userId) {
String sql = "SELECT * FROM users WHERE user_id =?";
return jdbcTemplate.queryForMap(sql, userId);
}
这里使用queryForMap方法,它返回一个包含查询结果的Map,键为列名,值为对应列的值。
4.1.2 插入操作
使用jdbcTemplate.update方法执行插入语句。例如,插入一条用户记录:
public void insertUser(User user) {
String sql = "INSERT INTO users (name, age) VALUES (?,?)";
jdbcTemplate.update(sql, user.getName(), user.getAge());
}
在批量插入场景中,batchUpdate方法能显著提高效率。比如批量插入用户数据:
public void batchInsertUsers(List<User> userList) {
String sql = "INSERT INTO users (name, age) VALUES (?,?)";
jdbcTemplate.batchUpdate(sql, userList, userList.size(), (ps, user) -> {
ps.setString(1, user.getName());
ps.setInt(2, user.getAge());
});
}
在这个方法中,batchUpdate接收SQL语句、数据列表、批次大小以及一个回调函数。回调函数用于将每个用户的数据设置到预编译语句中,实现批量插入操作。
4.1.3 更新操作
更新操作同样使用jdbcTemplate.update方法。例如,更新用户的年龄:
public void updateUserAge(int userId, int newAge) {
String sql = "UPDATE users SET age =? WHERE user_id =?";
jdbcTemplate.update(sql, newAge, userId);
}
4.1.4 删除操作
使用jdbcTemplate.update方法执行删除语句。例如,根据用户ID删除用户:
public void deleteUserById(int userId) {
String sql = "DELETE FROM users WHERE user_id =?";
jdbcTemplate.update(sql, userId);
}
4.2 整合MyBatis框架
MyBatis是一款优秀的持久化框架,它支持自定义SQL、存储过程以及高级映射,提供了更加灵活和强大的SQL映射功能。Spring与MyBatis的整合,充分发挥了两者的优势,使得数据库操作更加便捷。
首先,在pom.xml中添加MyBatis和MyBatis - Spring依赖:
<dependencies>
<dependency>
<groupId>org.mybatis.spring</groupId>
<artifactId>mybatis - spring</artifactId>
<version>3.0.1</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.10</version>
</dependency>
</dependencies>
这两个依赖分别引入了MyBatis与Spring的整合模块以及MyBatis核心模块。
接着,配置MyBatis的SqlSessionFactory和Mapper扫描:
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="mapperLocations" value="classpath:mapper/*.xml"/>
</bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.example.mapper"/>
</bean>
sqlSessionFactory用于创建SqlSession,它依赖于之前配置的数据源,并指定了Mapper XML文件的位置。MapperScannerConfigurer则用于扫描指定包下的Mapper接口,将其自动注册为Spring的Bean。
4.2.1 查询操作
创建Mapper接口和XML映射文件,如UserMapper.java和UserMapper.xml:
package com.example.mapper;
import com.example.User;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface UserMapper {
List<User> getUsers();
}
<?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.example.mapper.UserMapper">
<select id="getUsers" resultType="com.example.User">
SELECT * FROM users
</select>
</mapper>
在实际应用中,可能需要根据条件查询用户。比如根据用户姓名模糊查询用户列表:
package com.example.mapper;
import com.example.User;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface UserMapper {
List<User> getUsersByName(String name);
}
<?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.example.mapper.UserMapper">
<select id="getUsersByName" resultType="com.example.User">
SELECT * FROM users WHERE name LIKE '%${name}%'
</select>
</mapper>
这里使用了MyBatis的动态SQL功能,通过${name}将参数传入SQL语句中。
service层调用代码:
package com.example.service;
import com.example.mapper.UserMapper;
import com.example.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public List<User> getAllUsers() {
return userMapper.getUsers();
}
public List<User> getUsersByName(String name) {
return userMapper.getUsersByName(name);
}
}
测试代码:
使用JUnit 5进行测试,首先在pom.xml中添加JUnit 5依赖:
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.8.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.8.2</version>
<scope>test</scope>
</dependency>
</dependencies>
然后编写测试类:
package com.example;
import com.example.mapper.UserMapper;
import com.example.service.UserService;
import com.example.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
public class UserServiceTest {
@Autowired
private UserService userService;
@Autowired
private UserMapper userMapper;
@Test
public void testGetAllUsers() {
List<User> users = userService.getAllUsers();
assertNotNull(users);
assertTrue(users.size() >= 0);
}
@Test
public void testGetUsersByName() {
String testName = "John";
List<User> users = userService.getUsersByName(testName);
assertNotNull(users);
if (!users.isEmpty()) {
for (User user : users) {
assertTrue(user.getName().contains(testName));
}
}
}
}
在上述测试代码中,@SpringBootTest注解用于启动Spring应用上下文,使得可以在测试环境中注入和使用Spring管理的Bean。testGetAllUsers方法测试获取所有用户的功能,testGetUsersByName方法测试根据用户姓名模糊查询用户列表的功能。通过断言来验证返回结果的正确性,确保业务逻辑的准确性。
4.2.2 插入操作
在Mapper接口中定义插入方法:
package com.example.mapper;
import com.example.User;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper {
void insertUser(User user);
}
在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.example.mapper.UserMapper">
<insert id="insertUser">
INSERT INTO users (name, age) VALUES (#{name}, #{age})
</insert>
</mapper>
这里使用#{}来传递参数,它会自动进行SQL注入防护。
测试代码:
@Test
public void testInsertUser() {
User newUser = new User();
newUser.setName("Tom");
newUser.setAge(30);
userMapper.insertUser(newUser);
List<User> users = userMapper.getUsers();
assertNotNull(users);
boolean found = false;
for (User user : users) {
if (user.getName().equals("Tom") && user.getAge() == 30) {
found = true;
break;
}
}
assertTrue(found);
}
在这个测试方法中,先创建一个新用户对象,调用insertUser方法插入用户,然后获取所有用户列表,检查新插入的用户是否在列表中,以此验证插入操作的正确性。
4.2.3 更新操作
在Mapper接口中定义更新方法:
package com.example.mapper;
import com.example.User;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper {
void updateUser(User user);
}
在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.example.mapper.UserMapper">
<update id="updateUser">
UPDATE users SET name = #{name}, age = #{age} WHERE user_id = #{userId}
</update>
</mapper>
测试代码:
@Test
public void testUpdateUser() {
// 先插入一个用户,获取其user_id
User newUser = new User();
newUser.setName("Jerry");
newUser.setAge(25);
userMapper.insertUser(newUser);
List<User> usersBeforeUpdate = userMapper.getUsers();
int userId = usersBeforeUpdate.get(usersBeforeUpdate.size() - 1).getUserId();
// 更新用户信息
User updatedUser = new User();
updatedUser.setUserId(userId);
updatedUser.setName("Jerry Updated");
updatedUser.setAge(26);
userMapper.updateUser(updatedUser);
// 检查更新后的用户信息
List<User> usersAfterUpdate = userMapper.getUsers();
assertNotNull(usersAfterUpdate);
boolean found = false;
for (User user : usersAfterUpdate) {
if (user.getUserId() == userId && user.getName().equals("Jerry Updated") && user.getAge() == 26) {
found = true;
break;
}
}
assertTrue(found);
}
此测试方法先插入一个用户,获取其user_id,然后根据user_id更新用户信息,最后再次获取用户列表,检查更新后的用户信息是否正确,从而验证更新操作的有效性。
4.2.4 删除操作
在Mapper接口中定义删除方法:
package com.example.mapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper {
void deleteUserById(int userId);
}
在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.example.mapper.UserMapper">
<delete id="deleteUserById">
DELETE FROM users WHERE user_id = #{userId}
</delete>
</mapper>
测试代码:
@Test
public void testDeleteUserById() {
// 先插入一个用户,获取其user_id
User newUser = new User();
newUser.setName("Alice");
newUser.setAge(28);
userMapper.insertUser(newUser);
List<User> usersBeforeDelete = userMapper.getUsers();
int userId = usersBeforeDelete.get(usersBeforeDelete.size() - 1).getUserId();
// 删除用户
userMapper.deleteUserById(userId);
// 检查用户是否被删除
List<User> usersAfterDelete = userMapper.getUsers();
assertNotNull(usersAfterDelete);
boolean found = false;
for (User user : usersAfterDelete) {
if (user.getUserId() == userId) {
found = true;
break;
}
}
assertFalse(found);
}
该测试方法先插入一个用户,获取其user_id,然后根据user_id删除用户,最后获取用户列表,检查被删除的用户是否不在列表中,以此验证删除操作是否成功。
4.3 事务管理在数据库操作中的应用
在数据库操作里,事务管理起着关键作用,它保障了一组相关数据库操作的原子性,要么全部成功执行,要么全部失败回滚,进而确保数据的一致性和完整性。在Spring框架中,主要通过@Transactional注解和事务管理器来实现事务管理。
在已完成事务管理器配置的前提下,只需在数据库操作方法上添加@Transactional注解,就能确保多个数据库操作的原子性。例如,在用户服务中保存用户信息:
@Service
public class UserService {
@Autowired
private UserDao userDao;
@Transactional
public void saveUser(User user) {
// 插入用户记录
userDao.insertUser(user);
// 更新相关的统计信息,假设存在一个统计用户数量的表
userDao.updateUserCount();
}
}
在上述示例中,若插入用户记录后,更新用户数量统计信息时发生异常,由于saveUser方法被@Transactional注解标记,整个事务会回滚,用户记录不会被插入,从而保证数据的一致性。
在涉及资金操作的场景中,事务管理的重要性更为凸显。比如在在线支付系统中,用户支付订单的操作通常包含多个步骤:更新订单状态为已支付、扣除用户账户余额、增加商家账户余额等。
@Service
public class PaymentService {
@Autowired
private OrderDao orderDao;
@Autowired
private UserAccountDao userAccountDao;
@Autowired
private MerchantAccountDao merchantAccountDao;
@Transactional
public void processPayment(Payment payment) {
// 更新订单状态为已支付
orderDao.updateOrderStatus(payment.getOrderId(), "PAID");
// 扣除用户账户余额
userAccountDao.decreaseBalance(payment.getUserId(), payment.getAmount());
// 增加商家账户余额
merchantAccountDao.increaseBalance(payment.getMerchantId(), payment.getAmount());
}
}
在这个例子中,若扣除用户账户余额时出现异常,由于processPayment方法被@Transactional注解标记,整个事务会回滚,订单状态不会被更新为已支付,商家账户余额也不会增加,从而保证资金数据的准确性和一致性。
4.3.1 事务管理测试代码
使用JUnit 5对事务管理进行测试时,首先要确保pom.xml中已包含JUnit 5相关依赖。测试类示例如下:
package com.example;
import com.example.service.PaymentService;
import com.example.service.UserService;
import com.example.entity.Payment;
import com.example.entity.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
import static org.junit.jupiter.api.Assertions.assertThrows;
@SpringBootTest
public class TransactionTest {
@Autowired
private UserService userService;
@Autowired
private PaymentService paymentService;
@Test
@Transactional
public void testUserServiceTransaction() {
User user = new User();
user.setName("TestUser");
user.setAge(20);
// 模拟更新用户数量统计信息时抛出异常
assertThrows(RuntimeException.class, () -> {
userService.saveUser(user);
});
// 这里应该查询不到刚才插入的用户,因为事务回滚了
// 假设UserDao中有查询用户的方法getUserByName
// User insertedUser = userDao.getUserByName("TestUser");
// assertNull(insertedUser);
}
@Test
@Transactional
public void testPaymentServiceTransaction() {
Payment payment = new Payment();
payment.setOrderId(1);
payment.setUserId(1);
payment.setMerchantId(1);
payment.setAmount(100.0);
// 模拟扣除用户账户余额时抛出异常
assertThrows(RuntimeException.class, () -> {
paymentService.processPayment(payment);
});
// 这里应该查询到订单状态未更新为已支付,因为事务回滚了
// 假设OrderDao中有查询订单状态的方法getOrderStatusById
// String orderStatus = orderDao.getOrderStatusById(1);
// assertNotEquals("PAID", orderStatus);
}
}
在上述测试代码中,@SpringBootTest注解用于启动Spring应用上下文,这样就能在测试环境中注入和使用Spring管理的Bean。@Transactional注解添加在测试方法上,能保证测试方法执行完后事务回滚,避免影响数据库的实际数据。testUserServiceTransaction方法用于测试UserService中的事务,通过模拟更新用户数量统计信息时抛出异常,来验证用户记录不会被插入;testPaymentServiceTransaction方法用于测试PaymentService中的事务,通过模拟扣除用户账户余额时抛出异常,来验证订单状态不会被更新为已支付,以此保证事务管理在实际应用中的正确性和可靠性。
在实际应用中,事务管理在许多场景都发挥着关键作用。例如在电商系统中,一次订单创建操作可能涉及多个数据库表的修改,如订单表、库存表、用户积分表等。只有所有这些操作都成功完成,才能保证业务正常进行。若其中任何一个操作失败,事务回滚机制可确保之前已执行的操作被撤销,避免数据不一致的情况出现。
以简单的转账场景为例,假设存在两个账户AccountA和AccountB,要实现从AccountA向AccountB转账的功能,就需要对两个账户的余额进行更新操作。若其中一个更新操作失败,可能导致资金丢失或错误增加,所以必须使用事务管理来保障操作的原子性。
定义账户实体类和Mapper接口
// Account.java
package com.example;
public class Account {
private Integer id;
private String accountName;
private Double balance;
// 省略getter和setter方法
}
// AccountMapper.java
package com.example.mapper;
import com.example.Account;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface AccountMapper {
void updateBalance(Account account);
}
在AccountMapper.xml中编写SQL语句
<?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.example.mapper.AccountMapper">
<update id="updateBalance">
UPDATE account SET balance = #{balance} WHERE id = #{id}
</update>
</mapper>
创建服务类并添加事务注解
// TransferService.java
package com.example.service;
import com.example.Account;
import com.example.mapper.AccountMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class TransferService {
@Autowired
private AccountMapper accountMapper;
@Transactional
public void transfer(Integer fromAccountId, Integer toAccountId, Double amount) {
// 获取转出账户
Account fromAccount = new Account();
fromAccount.setId(fromAccountId);
// 假设查询余额的方法已在其他地方实现,这里先简单设置余额为100.0
fromAccount.setBalance(100.0);
// 转出金额
fromAccount.setBalance(fromAccount.getBalance() - amount);
accountMapper.updateBalance(fromAccount);
// 获取转入账户
Account toAccount = new Account();
toAccount.setId(toAccountId);
// 假设查询余额的方法已在其他地方实现,这里先简单设置余额为50.0
toAccount.setBalance(50.0);
// 转入金额
toAccount.setBalance(toAccount.getBalance() + amount);
accountMapper.updateBalance(toAccount);
}
}
编写测试代码验证事务正确性
// TransferServiceTest.java
package com.example;
import com.example.service.TransferService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
public class TransferServiceTest {
@Autowired
private TransferService transferService;
@Test
@Transactional
public void testTransfer() {
Integer fromAccountId = 1;
Integer toAccountId = 2;
Double amount = 20.0;
try {
transferService.transfer(fromAccountId, toAccountId, amount);
// 这里可以添加更多断言来验证转账后账户余额的正确性
// 由于实际余额查询方法未完整实现,暂不做具体余额断言
assertTrue(true);
} catch (Exception e) {
// 如果事务正常回滚,这里应该不会捕获到异常
fail("Transfer should not throw exception");
}
}
}
在这个测试方法中,调用transferService.transfer方法进行转账操作。由于transfer方法上有@Transactional注解,若转账过程中出现异常,整个事务会回滚。通过断言和异常捕获来验证事务的正确性,若事务回滚机制正常工作,转账操作失败时就不会对数据库中的账户余额造成不一致的影响。
五、Spring MVC Web开发
5.1 Spring MVC架构原理
Spring MVC是基于MVC(Model - View - Controller)设计模式构建的Web框架,它将应用程序分为三个主要部分:模型(Model)、视图(View)和控制器(Controller),各部分之间职责清晰,协同工作以处理用户请求并返回响应。
在Spring MVC中,前端控制器(DispatcherServlet)是整个流程的核心。当一个HTTP请求到达应用程序时,首先会被DispatcherServlet捕获。DispatcherServlet负责接收请求,并根据请求的URL将其分发给对应的处理器映射(HandlerMapping)。处理器映射会根据请求的URL找到相应的控制器(Controller),并返回一个包含处理器(Handler)和拦截器(Interceptor)的执行链。
控制器是处理业务逻辑的核心组件,它接收DispatcherServlet传递的请求,调用相应的业务服务(Service)进行业务处理,然后将处理结果封装成模型(Model),并返回一个逻辑视图名(View Name)。
视图解析器(ViewResolver)负责根据逻辑视图名解析出实际的视图(View)。视图可以是JSP、Thymeleaf模板、FreeMarker模板等多种形式。视图将模型中的数据渲染成用户界面,最终返回给客户端。
5.2 控制器、视图解析器、模型的使用
5.2.1 控制器
在Spring MVC中,控制器通常是一个被@Controller注解标记的类。例如,创建一个简单的用户控制器:
package com.example.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class UserController {
@GetMapping("/user")
public String getUser(Model model) {
// 模拟从数据库获取用户信息
String username = "John";
model.addAttribute("username", username);
return "user";
}
}
在上述代码中,@GetMapping("/user")注解表示当接收到/user的GET请求时,会调用getUser方法。Model用于传递数据到视图,这里将username添加到模型中。方法返回的"user"是逻辑视图名。
在实际电商项目中,展示商品列表是常见需求。假设我们有一个商品服务ProductService用于获取商品列表,创建如下控制器:
package com.example.controller;
import com.example.service.ProductService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class ProductListController {
private final ProductService productService;
public ProductListController(ProductService productService) {
this.productService = productService;
}
@GetMapping("/products")
public String getProductList(Model model) {
var products = productService.getAllProducts();
model.addAttribute("products", products);
return "productList";
}
}
在这个例子中,ProductListController通过构造函数注入ProductService,在getProductList方法中调用服务获取商品列表,添加到模型后返回productList视图,用于展示商品信息。
5.2.2 视图解析器
在Spring MVC的配置文件中配置视图解析器,以InternalResourceViewResolver为例:
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB - INF/views/"/>
<property name="suffix" value=".jsp"/>
</bean>
上述配置表示,视图解析器会将逻辑视图名前加上/WEB - INF/views/,后加上.jsp,从而得到实际的视图文件路径。例如,逻辑视图名user会被解析为/WEB - INF/views/user.jsp。
若使用Thymeleaf作为视图技术,需先在pom.xml添加依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
然后在Spring Boot的配置文件application.properties中配置Thymeleaf:
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
spring.thymeleaf.mode=HTML
这样,逻辑视图名会被解析为classpath:/templates/目录下对应的.html文件。例如,逻辑视图名productList会被解析为classpath:/templates/productList.html。
5.2.3 模型
模型是用于在控制器和视图之间传递数据的载体。除了前面示例中使用Model.addAttribute方法添加数据外,还可以使用ModelAndView对象。例如:
package com.example.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.servlet.ModelAndView;
@Controller
public class ProductController {
@GetMapping("/product")
public ModelAndView getProduct() {
ModelAndView mav = new ModelAndView();
mav.setViewName("product");
mav.addObject("productName", "Spring Boot Book");
return mav;
}
}
这里通过ModelAndView同时设置了逻辑视图名和模型数据。
在订单处理场景中,假设我们要展示订单详情,创建如下控制器:
package com.example.controller;
import com.example.model.Order;
import com.example.service.OrderService;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.servlet.ModelAndView;
@Controller
public class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@GetMapping("/order/{orderId}")
public ModelAndView getOrderDetails(@PathVariable Long orderId) {
Order order = orderService.getOrderById(orderId);
ModelAndView mav = new ModelAndView();
mav.setViewName("orderDetails");
mav.addObject("order", order);
return mav;
}
}
在getOrderDetails方法中,通过@PathVariable获取订单ID,调用OrderService获取订单详情,使用ModelAndView将订单数据和视图名传递给视图,用于展示订单详细信息。
5.3 处理HTTP请求与响应
5.3.1 处理HTTP请求
Spring MVC通过@RequestMapping及其衍生注解(如@GetMapping、@PostMapping等)来处理不同类型的HTTP请求。例如,处理POST请求:
package com.example.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;
@Controller
public class LoginController {
@PostMapping("/login")
public ModelAndView login(@RequestParam String username, @RequestParam String password) {
ModelAndView mav = new ModelAndView();
if ("admin".equals(username) && "123456".equals(password)) {
mav.setViewName("success");
} else {
mav.setViewName("error");
}
return mav;
}
}
@RequestParam用于获取请求参数,这里获取username和password参数进行登录验证。
在文件上传场景中,创建如下控制器:
package com.example.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.ModelAndView;
import java.io.File;
import java.io.IOException;
@Controller
public class FileUploadController {
@PostMapping("/upload")
public ModelAndView uploadFile(@RequestParam("file") MultipartFile file) {
ModelAndView mav = new ModelAndView();
if (!file.isEmpty()) {
try {
String uploadPath = "uploads/" + file.getOriginalFilename();
File dest = new File(uploadPath);
file.transferTo(dest);
mav.setViewName("uploadSuccess");
} catch (IOException e) {
mav.setViewName("uploadError");
}
} else {
mav.setViewName("uploadError");
}
return mav;
}
}
在uploadFile方法中,通过@RequestParam获取上传的文件,将文件保存到指定目录,根据上传结果返回不同视图。
5.3.2 处理HTTP响应
Spring MVC的响应处理主要包括返回视图和返回数据。返回视图如前面示例所示,而返回数据可以使用@ResponseBody注解。例如,返回JSON数据:
package com.example.controller;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
public class DataController {
@GetMapping(value = "/data", produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public Map<String, Object> getData() {
Map<String, Object> data = new HashMap<>();
data.put("message", "Hello, Spring MVC!");
data.put("status", "success");
return data;
}
}
@RestController注解相当于@Controller和@ResponseBody的组合,@GetMapping的produces属性指定了返回数据的媒体类型为JSON。
在获取用户信息接口场景中,假设我们有一个用户服务UserService,创建如下控制器:
package com.example.controller;
import com.example.model.User;
import com.example.service.UserService;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserDataController {
private final UserService userService;
public UserDataController(UserService userService) {
this.userService = userService;
}
@GetMapping(value = "/users/{userId}", produces = MediaType.APPLICATION_JSON_VALUE)
public User getUser(@PathVariable Long userId) {
return userService.getUserById(userId);
}
}
在getUser方法中,通过@PathVariable获取用户ID,调用UserService获取用户信息并直接返回,由于@RestController注解,返回的用户对象会自动转换为JSON格式响应给客户端。
六、Spring Boot快速开发
6.1 Spring Boot简介与优势
Spring Boot 是由 Pivotal 团队提供的全新框架,其设计目的是用来简化新 Spring 应用的初始搭建以及开发过程。它遵循“约定优于配置”的原则,提供了各种启动器(Starter)来简化依赖管理,并且内嵌了 Tomcat、Jetty 等 Servlet 容器,使得开发人员可以快速构建独立的、生产级别的 Spring 应用。
Spring Boot 的优势主要体现在以下几个方面:
- 快速搭建:通过 Spring Initializr 或 Maven/Gradle 快速创建项目,减少样板代码。
- 自动配置:根据项目依赖自动配置 Spring 应用,无需手动编写大量配置文件。
- 嵌入式容器:可以将应用打包成可执行的 JAR 或 WAR 文件,直接运行,无需外部 Servlet 容器。
- 生产就绪:提供了监控、健康检查等生产级特性,方便管理和维护应用。
6.2 自动配置原理与定制
自动配置原理
Spring Boot 的自动配置是基于条件注解(@Conditional)实现的。当项目中引入了某些依赖时,Spring Boot 会根据这些依赖自动配置相应的 Bean。例如,当引入了 Spring Web 依赖时,Spring Boot 会自动配置一个嵌入式的 Servlet 容器(如 Tomcat)和 Spring MVC 的相关组件。
自动配置的核心是 @SpringBootApplication 注解,它包含了 @EnableAutoConfiguration 注解,用于启用自动配置功能。
定制自动配置
如果默认的自动配置不符合项目需求,可以通过以下方式进行定制:
- 排除自动配置类:使用
@SpringBootApplication注解的exclude属性排除不需要的自动配置类。
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
- 自定义配置类:创建自己的配置类,使用
@Configuration注解,并在其中定义需要的 Bean。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MyConfig {
@Bean
public MyService myService() {
return new MyService();
}
}
6.3 构建 RESTful API 示例
项目创建
使用 Spring Initializr(start.spring.io/) 创建一个新的 Spring Boot 项目,选择以下依赖:
- Spring Web
示例代码
创建一个简单的实体类 User:
package com.example.demo.entity;
public class User {
private Long id;
private String name;
public User() {
}
public User(Long id, String name) {
this.id = id;
this.name = name;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
创建一个控制器 UserController 来处理 RESTful 请求:
package com.example.demo.controller;
import com.example.demo.entity.User;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
@RestController
@RequestMapping("/users")
public class UserController {
private List<User> users = new ArrayList<>();
// 获取所有用户
@GetMapping
public List<User> getAllUsers() {
return users;
}
// 根据 ID 获取用户
@GetMapping("/{id}")
public User getUserById(@PathVariable Long id) {
return users.stream()
.filter(user -> user.getId().equals(id))
.findFirst()
.orElse(null);
}
// 创建用户
@PostMapping
public User createUser(@RequestBody User user) {
users.add(user);
return user;
}
// 更新用户
@PutMapping("/{id}")
public User updateUser(@PathVariable Long id, @RequestBody User updatedUser) {
for (int i = 0; i < users.size(); i++) {
User user = users.get(i);
if (user.getId().equals(id)) {
user.setName(updatedUser.getName());
users.set(i, user);
return user;
}
}
return null;
}
// 删除用户
@DeleteMapping("/{id}")
public void deleteUser(@PathVariable Long id) {
users.removeIf(user -> user.getId().equals(id));
}
}
测试代码
使用 JUnit 和 Spring Boot Test 进行单元测试:
package com.example.demo.controller;
import com.example.demo.entity.User;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@SpringBootTest
@AutoConfigureMockMvc
public class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Test
public void testCreateUser() throws Exception {
User user = new User(1L, "John Doe");
String userJson = objectMapper.writeValueAsString(user);
mockMvc.perform(post("/users")
.contentType(MediaType.APPLICATION_JSON)
.content(userJson))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.name").value("John Doe"));
}
}
七、Spring Cloud微服务开发
7.1 微服务架构概念
微服务架构是一种将单个应用程序拆分成多个小型、自治的服务的架构风格。每个服务都可以独立开发、部署和扩展,通过轻量级的通信机制(如 HTTP/REST)进行交互。微服务架构的主要优势包括:
- 可扩展性:可以根据业务需求独立扩展各个服务。
- 灵活性:不同的服务可以使用不同的技术栈。
- 易于维护:每个服务的代码量相对较小,易于理解和维护。
7.2 Spring Cloud组件介绍
Eureka
Eureka 是 Netflix 开发的服务注册与发现组件,用于实现微服务的注册与发现功能。服务提供者将自己的信息注册到 Eureka Server,服务消费者从 Eureka Server 获取服务提供者的信息。
Ribbon
Ribbon 是 Netflix 开发的客户端负载均衡组件,用于在多个服务实例之间进行负载均衡。它可以根据不同的负载均衡策略(如轮询、随机等)选择合适的服务实例。
Feign
Feign 是一个声明式的 HTTP 客户端,用于简化服务之间的调用。通过定义接口和注解,Feign 可以自动生成 HTTP 请求代码。
7.3 微服务的注册与发现、负载均衡实现
项目创建
创建两个 Spring Boot 项目:eureka-server 和 service-provider、service-consumer。
Eureka Server 配置
在 eureka-server 项目的 pom.xml 中添加以下依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
在 application.yml 中进行配置:
server:
port: 8761
eureka:
client:
register-with-eureka: false
fetch-registry: false
创建启动类并添加 @EnableEurekaServer 注解:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
服务提供者配置
在 service-provider 项目的 pom.xml 中添加以下依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
在 application.yml 中进行配置:
server:
port: 8081
spring:
application:
name: service-provider
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
创建一个简单的控制器:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "Hello from service provider!";
}
}
服务消费者配置
在 service-consumer 项目的 pom.xml 中添加以下依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
在 application.yml 中进行配置:
server:
port: 8082
spring:
application:
name: service-consumer
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
创建 Feign 客户端接口:
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
@FeignClient(name = "service-provider")
public interface HelloClient {
@GetMapping("/hello")
String hello();
}
创建控制器调用 Feign 客户端:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ConsumerController {
@Autowired
private HelloClient helloClient;
@GetMapping("/consumer")
public String consumer() {
return helloClient.hello();
}
}
测试
启动 eureka-server、service-provider 和 service-consumer 项目,访问 http://localhost:8082/consumer,可以看到服务消费者调用服务提供者的结果。
八、Spring实战项目
8.1 完整项目需求分析与设计
假设我们要开发一个简单的图书管理系统,主要功能包括:
- 用户可以查看图书列表。
- 用户可以添加、修改和删除图书信息。
功能模块设计
- 图书管理模块:负责图书信息的增删改查操作。
- 用户界面模块:提供用户交互界面,展示图书列表和操作按钮。
数据库设计
设计一个 books 表,包含以下字段:
id:图书 ID,主键。title:图书标题。author:图书作者。price:图书价格。
8.2 各模块使用 Spring 技术实现
项目创建
使用 Spring Initializr 创建一个新的 Spring Boot 项目,选择以下依赖:
- Spring Web
- Spring Data JPA
- MySQL Driver
数据库配置
在 application.yml 中配置数据库连接信息:
spring:
datasource:
url: jdbc:mysql://localhost:3306/bookstore
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: update
show-sql: true
实体类设计
创建 Book 实体类:
package com.example.bookstore.entity;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String author;
private double price;
public Book() {
}
public Book(String title, String author, double price) {
this.title = title;
this.author = author;
this.price = price;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
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;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
}
数据访问层设计
创建 BookRepository 接口:
package com.example.bookstore.repository;
import com.example.bookstore.entity.Book;
import org.springframework.data.jpa.repository.JpaRepository;
public interface BookRepository extends JpaRepository<Book, Long> {
}
服务层设计
创建 BookService 类:
package com.example.bookstore.service;
import com.example.bookstore.entity.Book;
import com.example.bookstore.repository.BookRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class BookService {
@Autowired
private BookRepository bookRepository;
public List<Book> getAllBooks() {
return bookRepository.findAll();
}
public Book addBook(Book book) {
return bookRepository.save(book);
}
public Book updateBook(Long id, Book updatedBook) {
Book book = bookRepository.findById(id).orElse(null);
if (book != null) {
book.setTitle(updatedBook.getTitle());
book.setAuthor(updatedBook.getAuthor());
book.setPrice(updatedBook.getPrice());
return bookRepository.save(book);
}
return null;
}
public void deleteBook(Long id) {
bookRepository.deleteById(id);
}
}
控制器层设计
创建 BookController 类:
package com.example.bookstore.controller;
import com.example.bookstore.entity.Book;
import com.example.bookstore.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/books")
public class BookController {
@Autowired
private BookService bookService;
@GetMapping
public List<Book> getAllBooks() {
return bookService.getAllBooks();
}
@PostMapping
public Book addBook(@RequestBody Book book) {
return bookService.addBook(book);
}
@PutMapping("/{id}")
public Book updateBook(@PathVariable Long id, @RequestBody Book updatedBook) {
return bookService.updateBook(id, updatedBook);
}
@DeleteMapping("/{id}")
public void deleteBook(@PathVariable Long id) {
bookService.deleteBook(id);
}
}
8.3 项目部署与优化
项目部署
将项目打包成可执行的 JAR 文件,使用以下命令:
mvn clean package
在服务器上运行 JAR 文件:
java -jar target/bookstore-0.0.1-SNAPSHOT.jar
项目优化
- 缓存优化:使用 Redis 等缓存技术,减少数据库访问次数。
- 性能监控:使用 Spring Boot Actuator 进行性能监控,及时发现和解决性能问题。
- 日志管理:使用 Logback 等日志框架,对日志进行有效管理。