Spring:整体思路讲解

Spring:整体思路讲解

Spring整体思路讲解

后端,说白了就是围绕数据库做增删查改:从数据库中获取数据,处理后返回给前端;或者接收前端传来的数据,处理后存入数据库。这样前后端协同,项目就能跑起来。

比如实现一个注册登录功能:

  • 注册:用户填写手机号、邮箱、密码等信息,前端传给后端。后端用一个“用户注册类”接收这些字段,然后将其存入数据库的用户表中。

  • 登录:用户输入账号密码,后端接收后去数据库查询,先判断用户名是否存在,再比对密码是否正确。匹配成功就登录成功,否则返回错误提示。

看起来就是简单的数据交互,但实际上后端远不止这些。

真正复杂的地方在于“如何管理用户状态与系统安全”:

  • 用户登录后,如何让系统知道“这个请求是谁发的”?

  • 用户没登录,却直接访问别人主页的链接,要如何阻止?

  • 用户登录之后要返回什么信息给前端?Token?Session?

  • 后端是要每次都手写判断,还是有统一拦截机制来管理这些权限?

  • 数据库压力大时如何优化查询?怎么加缓存?用户量大了怎么拆服务?

  • 日志怎么打?异常怎么监控?接口怎么设计才能稳、扩展性强?

这些问题构成了后端的核心价值:权限控制、接口设计、缓存优化、消息队列、负载均衡、日志管理、安全校验、服务拆分……Spring看起来“复杂”,其实就是把这些基础能力都封装好了,让我们不用重复造轮子

如何用Spring的语言操作这些东西

首先我们先解决增删查改的问题

现在程序的核心思想就是解耦,什么是解耦呢,就是把不同模块的功能分开进行处理,比如和前端打交道的模块,处理数据的模块,以及和数据库打交道的模块,还有单独用来
存储数据的类
这些模块分别叫做
Controller 层(控制层)
Service 层(服务层)
Repository 层 / DAO 层(数据访问层)现在叫Mapper(以后会说为什么,有什么区别)(挖坑
Entity / POJO / DTO

接下来我们按照接收数据,处理数据,数据库交互的思路来进行讲解

Controller 层(控制层)

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
@RequestMapping("/user")
public class UserController {

@Autowired
private UserService userService;

@PostMapping("/register")
public Result register(@RequestBody UserDTO userDTO) {
return userService.register(userDTO);
}
}

看到这个代码初学者指定懵逼了
@RestController这些是什么,这是注解,Java自带的语言特性,贴个代码你就想起来了

1
2
3
4
5
6
7
8
9
10
11
public class Demo {
@Override
public String toString() {
return "这是个重写";
}

@Deprecated
public void oldMethod() {
System.out.println("不建议再用我了");
}
}

Spring 和很多框架是基于 Java 原生注解机制,自定义了大量注解,比如:

注解作用举例
@RestController声明这个类是一个 REST 接口类(自动返回 JSON)贴在类上
@RequestMapping("/user")给这个类或方法绑定访问路径可写在类或方法上
@PostMapping("/register")绑定 POST 请求的具体路径只贴在方法上
@RequestBody把 JSON 转成 Java 对象用在参数前
@GetMapping, @DeleteMapping, @PutMapping对应不同的 HTTP 请求类似 PostMapping

这些不用一个个记忆,我们结合代码就方便理解了

1
2
3
4
5
6
7
8
9
@RestController // 这是个对外暴露的接口类
@RequestMapping("/user") // 所有路径都以 /user 开头
public class UserController {

@PostMapping("/register") // 注册接口,POST 请求
public String register(@RequestBody UserDTO userDTO) {
return "注册成功:" + userDTO.getUsername();
}
}
PostMapping是什么?
接口是什么?

Controller层和前端打交道所使用的方式就是接口
上文中的RequestMapping(“/user”)是UserController接口类的一个接口“根”路径
@PostMapping(“/register”)是一个post类型的接口
简单讲就是当前端请求「服务器地址+/user/register」的时候,后端就会自动识别到这个请求,然后执行@PostMapping(“/register”) 下面的方法
可以理解为遥控器的按钮,当按下“音量+”的时候,电视的音量就会升高。

如何引入Service层?
三种方式

@Autowired 注解是 Spring 中用来自动注入依赖的常见方式,我们先使用字段注入,后期对这三种方式进行细说(挖坑
1.1 构造器注入(推荐方式)
1.2 字段注入(最常见的方式)
1.3 Setter 方法注入

1
2
@Autowired
private UserService userService;
UserService是怎么被发现的?

这不得不提注解的知识了
当我们使用 @Component及其衍生注解@Service@Repository@Controller的时候,Spring 会自动扫描指定包下的类,并根据这些注解将它们注册为
Spring 管理的 Bean,这一系列操作也就是Spring 的组件扫描(Component Scanning)(挖坑

怎么返回值?

前端通常接收到的数据为:

1
2
3
4
5
6
7
{
code: 1
"msg": "success"
"data": {
xxx
}
}

在我们的第一个代码示例中出现了@RestController注解
它是 @RestController@ResponseBody的合成注解
@RestController的作用就是方法返回的对象会自动转换成 JSON
这里引入一个 HTTP 消息转换器(HttpMessageConverter) 机制

  • 把 Java 对象 ➜ 转成 JSON(发送给前端)
  • 把前端传来的 JSON ➜ 还原成 Java 对象(用在 @RequestBody 上)
    默认情况下,Spring Boot 会自动引入 Jackson(一个 JSON 序列化库)来进行转换。
    以后会详细讲解这些(挖坑
实践
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController
@RequestMapping("/admin/user")
@Slf4j
@Autowired
private UserService userService;

@Autowired
private UserMapper userMapper;
@PostMapping("/register")
public Result register(@RequestBody UserDTO userDTO, String code) throws BaseException {
log.info("新增用户{}", userDTO);
Integer result = userService.registerUser(userDTO, code);
if (result == 1) {
return Result.success();
} else return Result.error("添加失败");
}
BaseException是什么,怎么定义的?

Spring:使用自定义异常

code是什么,干什么的?

Spring:邮箱验证码

为什么前面会带一个@RequestBody?

消息转换器(挖坑
因为前端通过 JSON 传数据时,不是传统的表单形式,Spring 不知道该怎么自动映射这些数据。
只有加了 @RequestBody,Spring 才会用 消息转换器(HttpMessageConverter),把 JSON ➜ 转成 Java 对象。

1
2
3
4
5
6
POST /register
Content-Type: application/json
{
"username": "岩羊",
"password": "123456"
}
1
2
3
4
5
@PostMapping("/register")
public Result register(@RequestBody UserDTO userDTO, String code) {
// userDTO 就是前端传来的 JSON 自动转的对象
// code 可以是 URL 参数或 form-data 传的
}

接收什么数据该用什么注解呢?(挖坑

Service 层

Service层主要用来进行业务处理,就如同上文提到的,被Controller层进行调用,从而处理前端的各种数据,然后把处理好的数据提供给Mapper,存储到数据库中

1
2
3
4
5
6
7
8
9
10
11
@Service
public class UserServiceImpl implements UserService {

@Autowired
private UserMapper userMapper;

public boolean login(String username, String password) {
User user = userMapper.findByUsername(username);
return user != null && user.getPassword().equals(password);
}
}
1
2
3
public interface UserService {  
Integer registerUser(UserDTO userDTO, String code) throws BaseException;
...

整体一看,Service层有接口和实现类

为什么使用接口和实现类呢?

这是面向接口编程 的体现
如果你直接写死

1
2
@Autowired
private UserServiceImpl userService;

这就直接具体实现了,如果将来我想要写个测试Service,就需要到处进行修改了
有了接口,我们可以用一个Service实现不同环境的逻辑
比如

1
2
public class MockUserServiceImpl implements UserService{}// 测试使用
public class RemoteUserService implements UserService{}// 远程服务

由于Service层主要体现在对数据的处理方面

实践
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
@Service  
@Slf4j
public class UserServiceImpl implements UserService {
@Autowired
RoleMapper roleMapper;
@Autowired
UserMapper userMapper;
@Autowired
private RedisUtil redisUtil;

@Override
public Integer registerUser(UserDTO userDTO, String code) throws BaseException {
// 格式校验
// 校验用户名
if (!isValidUsername(userDTO.getUsername())) {
throw new BaseException("无效的用户名,用户名必须是3-20个字符,且只能包含字母、数字、下划线和短横线。");
}
// 校验用户名是否重复
UserPO temp = new UserPO();
temp.setUsername(userDTO.getUsername());
if (userMapper.select(temp) != null) throw new BaseException("用户名" + userDTO.getUsername() + "重复");
// 校验邮箱
if (!isValidEmail(userDTO.getEmail())) {
throw new BaseException("无效的邮箱格式.");
}
// 校验邮箱是否重复
temp = new UserPO();
temp.setUsername(userDTO.getEmail());
if (userMapper.select(temp) != null) throw new BaseException("该邮箱已注册");
// 校验密码
if (!isValidPassword(userDTO.getPassword())) {
throw new BaseException("无效的密码。密码必须是6-20个字符。");
}
TypeReference<String> typeReference = new TypeReference<String>() {};
// 校验验证码
if (code.isEmpty()) {
throw new BaseException("请输入验证码");
}
if (redisUtil.get(userDTO.getEmail()+"code",typeReference) == null){
throw new BaseException("验证码已过期");
}
if (!code.equals(redisUtil.get(userDTO.getEmail()+"code",typeReference))) {
throw new BaseException("验证码错误");
}
UserPO userPO = new UserPO();
BeanUtils.copyProperties(userDTO, userPO);
// UUID
String userId = UUID.randomUUID().toString();
userPO.setId(userId);
// 默认头像
userPO.setAvatar("头像地址");
// 密码加密
userPO.setPassword(DigestUtils.md5DigestAsHex(userDTO.getPassword().getBytes()));
// 新增状态
userPO.setStatus(Status.NORMAL);
// 新增身份
userPO.setRole(Role.ORDINARY);
// 创建时间
userPO.setCreatedAt(LocalDateTime.now());
// 修改时间
userPO.setUpdatedAt(LocalDateTime.now());
log.info("Registering user: {}", userPO);
return userMapper.registerUser(userPO);
}

微距了吧,其实就注册类复杂一点,因为这种操作类似于用户初始化,你要在用户注册的时候为用户添加各种默认的信息
看似复杂,其实总体就三步,接收Controller层的数据,处理数据,把处理好的数据交给Mapper层。
接受信息不必多说,在 POJO 里创建一个用户信息的类,然后直接字段注入

UserPO示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Data  
@NoArgsConstructor
@AllArgsConstructor
public class UserPO {
private String id; // 用户ID,使用UUID
private String username; // 用户名
private String password; // 加密后的密码
private String email; // 邮箱,唯一
private String avatar; // 头像URL
private int status; // 用户状态,1为激活,0为禁用
private Integer role; // 用户角色,引用Role类
private LocalDateTime createdAt; // 创建时间
private LocalDateTime updatedAt; // 更新时间
}

前面校验部分在讲解Mapper之前了解即可,我们只需要知道经过一系列判断之后,我们把Controller层的数据全部放到了UserPO内进行存储,然后把数据交给Mapper层进行处理。

我们看到,Controller接收数据以及把数据交给Service层使用的是UserDTO,而Service把数据交给Mapper使用的则是PO,用什么区别呢?等讲
POJO 层的时候进行展开。

在Service中我们知道了如何从Controller接收数据,数据处理的逻辑是什么样的,以及如何把数据传给Mapper。

Mapper层

Mapper有两种和数据库交互的方法。
方式一:注解版
1
2
3
4
5
6
7
@Mapper
public interface UserMapper {
@Select("SELECT * FROM user WHERE username = #{username}")
User findByUsername(String username);
@Insert("INSERT INTO user (username, password) VALUES (#{username}, #{password})")
void insertUser(User user);
}

如你所见,只需要在方法上加一个注解,括号里写上相应的数据库逻辑,在被调用的时候就会执行命令。
如果命令很多很复杂,我们在注解括号里写不就很麻烦吗?

方式二:XML 配置(更灵活)
1
2
3
public interface UserMapper {
User findByUsername(String username);
}
1
2
3
4
<!-- resources/mapper/UserMapper.xml -->
<select id="findByUsername" resultType="User">
SELECT * FROM user WHERE username = #{username}
</select>

注意,这个xml映射文件要放在resources中,和UserMapper在一个结构中才能被映射到,比如
src/main/java/com/dawnlight/chronicle_dawnlight/mapper/UserMapper.java
src/main/resources/com/dawnlight/chronicle_dawnlight/mapper/UserMapper.xml
其中/com/dawnlight/chronicle_dawnlight/mapper/UserMapper.java这一段都是一样的,不言而喻(嗯

其中的id就是对应Mapper中的方法名,resultType就是返回值类型

实例
1
2
3
@Mapper  
public interface UserMapper {
Integer registerUser(@Param("user") UserPO user);
1
2
3
4
5
6
7
8
9
10
<?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.dawnlight.chronicle_dawnlight.mapper.UserMapper">
<insert id="registerUser" parameterType="com.dawnlight.chronicle_dawnlight.pojo.po.UserPO">``
INSERT INTO users (id, username, password, email, avatar, status, role, created_at, updated_at)
VALUES (#{user.id}, #{user.username}, #{user.password}, #{user.email},
#{user.avatar}, #{user.status}, #{user.role}, #{user.createdAt},
#{user.updatedAt});
</insert>

问:如何让Spring和数据库相连接呢?
没错,既然需要Mapper层操作数据库,我们首先就要和数据库建立连接。
连接方式非常简单,涉及到了Spring的配置文件的知识(挖坑
这里我们简单讲述一下对数据库的配置
默认的配置文件叫做application.properties
我们可以修改为application.yml,比较简洁高效

1
2
3
4
5
6
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://地址:端口号/构架名
username: root
password: 104754
问:@Param是什么

@Param(“user”)可以理解为我们给形参 UserPO user 起了一个名字叫做user,这样我们在 xml 中看到的

1
2
3
VALUES (#{user.id}, #{user.username}, #{user.password}, #{user.email}, 
#{user.avatar}, #{user.status}, #{user.role}, #{user.createdAt},
#{user.updatedAt});

这就让程序就知道了user是 你传入的 UserPO user

问: parameterType = “com.dawnlight.chronicle_dawnlight.pojo.po.UserPO”是什么

这句话的作用是定义传入参数的类型,指定为UserPO,同理,上文代码中的 resultType=”User” 是定义返回值类型

问:namespace = “com.dawnlight.chronicle_dawnlight.mapper.UserMapper”有什么用?

这是声明一个作用域,就是告诉这个xml文件,我们要找的Mapper文件在这个路径下,这样就能够根据Mapper中的方法名自动映射了。

问:这个代码返回值是什么呢?

这段 MyBatis 的 <insert> 标签,对应的返回值,其实是 执行成功后受影响的行数(int 类型)

补充:如果我们没有在Service中设置主键,而是设置的数据库自增主键,我们如何获取到这个自增的主键呢?
1
2
3
4
5

<insert id="registerUser" parameterType="UserPO" useGeneratedKeys="true" keyProperty="id">
INSERT INTO users (...)
VALUES (...)
</insert>

这样 MyBatis 会自动把数据库生成的主键 ID 回填到 user.id

回到上文,我们提到了校验用户名和邮箱等数据是否已经被使用,这个逻辑在Mapper中很好实现,无非就是使用select语句对用户名和邮箱这些数据进行查询,如果查询到了,就说明已经被使用了

1
String select(UserPO userPO);
1
2
3
4
5
6
7
8
9

<select id="select" resultType="java.lang.String">
select id
from users
<where>
<if test="username != null">and username = #{username}</if>
<if test="email != null">and email = #{email}</if>
</where>
</select>

这里我们把用户名查询和邮箱查询放到了一起,如果传的值是username,他就会查询username,邮箱同理

<where><if> 这些是什么呢?

我们又引入了一个知识点,mybatis中的动态sql多条件查询
em,这篇文章讲的比我好,自行查阅
MyBatis动态SQL 多条件查询

以上内容大致讲述了三层架构的作用以及具体代码的含义,当然,这只是Spring内容的一小部分。
按照以上操作可以完成一个基础的注册流程。
更多细节以后会更新,已经标注了(挖坑


Spring:整体思路讲解
https://www.zheep.top/posts/3458421192/
作者
西行寺岩羊
发布于
2025年5月6日
许可协议