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") public class UserController { @PostMapping("/register") 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) { }
接收什么数据该用什么注解呢?(挖坑
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); 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; private String username; private String password; private String email; private String avatar; private int status; private Integer 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 <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
这句话的作用是定义传入参数的类型,指定为UserPO,同理,上文代码中的 resultType=”User” 是定义返回值类型
这是声明一个作用域,就是告诉这个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内容的一小部分。 按照以上操作可以完成一个基础的注册流程。 更多细节以后会更新,已经标注了(挖坑