Spring-back-mini
后端小项目
吐槽 :
这个项目好难,还只是一个小的项目,很难想像大项目岂不是让我崩了,写了一堆不知道是啥的东西 ,本质就是一个登录的界面和注册,以及修改密码和管理的项目。
为啥感觉我这么简单的东西实现起来我真的想骂人啊!!!!真服了,现在找个项目练习都是怎么难的吗??我服了
spring 的理解:
- spring 就是一个框架 -- 完整的工具箱子,里面有各种各样的工具
- 管理对象的工具 -- Ioc容器
- 管理事务的工具 -- Transaction
- 管理数据库工具 -- JDBC
- 管理安全的工具 -- Security
- 托管Web工具 -- Spring MVC
SpringBoot 就是启动器 -- 给spring 这个工具箱添加的一个助手
- springboot 就是自己内嵌了 Tomcat 不用自己装服务器
- 自动配好了视图解析器,可以在application.yml中自己配置
- springboot 中的pom.xml是用来存放框架的,需要什么框架自己手动添加
- 启动springboot是在主函数中添加注解 @SpringBootApplication 的main 方法就可以直接运行了
spring-boot-starter-web这个依赖已经把spring mvc的所有jar包和默认配置全部打包好了
Redis 的配置
- redis 在存数据的安按照配置文件的形式存储
// redis 的配置文件类
@Configuration
public class RedisConfig {
@Bean // 将这个方法返回的对象 redisTemplate 交给 spring 容器管理,String 对应的就是key 类型, Object 对应的是 value型
public RedisTemplate<String , Object> redisTemplate(
// 这个是 spring boot 自动配置好的redis 连接工厂,包含了redis 需要连接的端口、密码等信息,将参数传递进去之后spring 自动会将connectionFactory 注入进去
@Autowired RedisConnectionFactory connectionFactory
){
// 这个是对 Key 转为普通的字符串,作用是在redis 看见的数据是明文的形式,而不是乱码的
StringRedisSerializer keySerializer = new StringRedisSerializer();
//这个是专门负责将Value 转换为Json格式的序列化器, Generic 是任意的意思,表示任意类型通用
GenericJackson2JsonRedisSerializer valueSerializer =
new GenericJackson2JsonRedisSerializer();
// 创建一个空的 RedisTempalte 实例
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// 把连接工厂设置进去 ,相当于告诉 redisTemplate 去操作redis的时候,用这个 地址和密码连接,如果没有是连接不上redis 的
redisTemplate.setConnectionFactory(connectionFactory);
// 修改序列化方式 为 json 格式
// Redis 有两种数据结构 : 普通键值对 String - String 类型 :Key 用 keySerializer , value 用的是 valueSerializer
// Hash 结构 (类似java 的 hashMap): hash的字段名用 hashKeySerializer , 字段值用是: hashValueSerializer
// 下面的语法也就是固定的
redisTemplate.setKeySerializer(keySerializer);
redisTemplate.setValueSerializer(valueSerializer);
redisTemplate.setHashKeySerializer(keySerializer);
redisTemplate.setHashValueSerializer(valueSerializer);
return redisTemplate;
}
}
WebConfig的配置
@Configuration // 配置类在 spring 启动的时候会读取它的内容
@RequiredArgsConstructor // Lombok 提供的注解 ,自动生成构造器。为所有的 final 字段生成构造函数
public class WebConfig implements WebMvcConfigurer {
// 定义的一个拦截器,用来校验用户是否登录、session是否有效
private final SessionInterceptor sessionInterceptor;
@Bean // 将返回的对象交给 spring 容器管理
// FilterRegistrationBean 是 spring boot 提供的一个注册器, 手动注册 Filter 过滤器,可以控制它的执行顺序和拦截路径
public FilterRegistrationBean<AuthSessionFilter> registrationBean(
// 按照名称"authSessionFilter"注入。因为 spring 容器中肯能有多个 AuthSessionFilter 类型的 Bean ,这里明确指定
@Qualifier("authSessionFilter") AuthSessionFilter authSessionFilter
// AuthSessionFilter 也是自定义过滤器用来处理 Session认证的
){
// 创建一个注册器实例, 把 authSessionFilter 放进去
FilterRegistrationBean<AuthSessionFilter> registrationBean
= new FilterRegistrationBean<>(authSessionFilter);
// 设置拦截路径:这个过滤器只对/user/* 路径下的请求生效也就是在/user/下的所有路径都生效
registrationBean.addUrlPatterns("/user/*");
// 将配置好的注册器返回给 Spring 容器 , spring boot 会自动读取它并且把 AuthSessionFilter 注册到Servlet 容器中
return registrationBean;
}
// 配置拦截器
@Override
public void addInterceptors(InterceptorRegistry registry){
registry.addInterceptor(sessionInterceptor) /// 把 SessionInterceptor注册到拦截器中
.addPathPatterns("/**") // 拦截所有的路径
.excludePathPatterns("/login" , "/register"); // 排除/login 和 /register 接口,也就是他们不需要经过Session校验,因为用户还没有登录,无法校验
}
}
Filter 和 Interceptor 的区别
| 对比维度 | Filter(过滤器) | Interceptor(拦截器) |
|---|---|---|
| 属于谁 | Servlet | 容器(Tomcat、Jetty 等) |
| 执行时机 | 请求进入 Servlet 之前,最外层 | 请求进入 Controller 之前,更靠近业务 |
| 拦截粒度 | 只能拦截 URL 路径(/user/*) | 可以更精细(按路径、按请求方法) |
| 能否获取 | Spring Bean 能,但需要额外步骤(通过 WebApplicationContextUtils) | 直接能,因为拦截器本身就是 Spring Bean,可以 @Autowired 其他 Bean |
| 能否处理 | ModelAndView 不能,只能在请求前后做处理 | 可以,可以在 postHandle 中修改返回的视图或数据 |
| 典型用途 | 字符编码、登录鉴权(JWT 校验)、日志记录、压缩响应 | 权限校验、Session 校验、性能监控、访问日志 |
统一的响应 遵守HTTP协议
// 后端统一处理接口
// 后端给前端返回数据是使用的统一格式,使得每次操作访问的时候后端都可以返回清晰的结构用户排查
@Setter // Lombok注解 自动生成setter 和 getter 方法
@Getter
public class R<T> {
private int code; // 200 400 500
private String message; // 消息 说明
private boolean success; // 是否成功
private T data; // 数据
private static final Map<String, Object> PRESENT = Collections.EMPTY_MAP;
public R(int code, String message, boolean success, T data) {
this.code = code;
this.message = message;
this.success = success;
this.data = data;
}
public R() {
}
public static <T> R<T> success(T data) {
return new R<>(200, "success", true, data);
}
public static R<?> success() {
return new R<>(200, "success", true, PRESENT);
}
public static R<?> fail(int code, String message) {
return new R<>(code, message, false, PRESENT);
}
public static R<?> fail(String message) {
return new R<>(500, message, false, PRESENT);
}
public static R<?> unauthoried(String message) {
return new R<>(401 , message , false , PRESENT);
}
}
统一异常处理
// 这个部分是在统一接口之后的最后一道防线,会拦截所有 Cotroller 层抛出的异常
@RestControllerAdvice // 给所有的Contrller层 装一个 监控,任何 controller层抛出的异常这个地方统一处理
public class GlobalExceptionHandler {
@ExceptionHandler //异常处理器
// 设置返回HTTP响应的状态码
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) // 500
// 这个确保返回的异常是符合HTTP 协议的,这样不会乱码
public R<?> handlerException(Exception e){
return R.fail(e.getMessage());
}
}
第一个功能: 注册
- 测试工具
Postman - 用户名需要唯一: redis 的快速失败机制 + 唯一约束
- 密码加密 (bcrypt 加密)
- 用户输入 JSON格式的 username 和password; 所以需要在 pojo 中增加一个 dto 层用户存放注册的数据
- 保存到 tb_user 表格中
// 负责处理相关的 http 请求
@RestController // 将所有的方法返回成 json的格式
@RequiredArgsConstructor // 对所以被 final 修饰的对象自动创建构造函数
public class RegisterController {
private final UserService userService;
@PostMapping("/register") // 指定 http 为 post 请求
// @RequestBody 将前端提交的 json 自动转换成 UserCreateDto 对象
public R<?> register(@RequestBody UserCreateDto dto) throws Exception{
// 将 dto 属性复制到 User 实体,也就是将对象数据转为数据库的实体对象
User user = BeanUtil.copyProperties(dto , User.class);
// Bcrypt 加密密码 ,防止彩虹表的攻击
user.setPassword(DigestUtil.bcrypt(dto.getPassword()));
// 设置当前时间为创建的时间
user.setCreateAt(LocalDateTime.now());
// 调用服务层保存对象
userService.saveUser(user);
// 返回统一成功响应
return R.success();
}
}
服务层结构是业务逻辑实现
@Service // 被spring 管理
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
// 数据访问层 执行 sql 语言操作
private final UserMapper userMapper;
// redis 客户端 ,用于 分布锁
private final RedisTemplate<String , Object> redisTemplate;
// reids 锁的值
private final static String PRESENT = "SAVE USER";
// redis 防止并发重复注册 isUse 后面的就是redis 的分布锁 30 秒之内不可以重复注册。30 秒之后mysql中的唯一约束发力,
@Override
public void saveUser(User user) {
// setIfAbsent = SETNX ;如果 key 值不存在就设置,存在就返回false, key 存储的是用户名
boolean isUse = redisTemplate.opsForValue().
setIfAbsent(user.getUsername() , PRESENT,30 , TimeUnit.SECONDS);
// 用户存在就说名已经注册过了
if (!isUse ){
throw new RuntimeException("用户已经注册过了!!!!!!");
}
// 用户名字唯一 : 可以在数据的 username 列创建一个唯一约束
// 插入数据库 , 对应的数据库 username 列需要建立 唯一索引作为保障
int userId = userMapper.insert(user);
System.out.println(userId); // 这个是用来调试输出的,在日志中比较重要
}
@Override
// 根据用户名来查询用户 (用于登录验证)
public User findUserByUsername(String username) throws Exception {
return userMapper.selectByUsername(username);
}
// 分页功能对应的参数是(当前是第几页, 每页的条数 , 查询的条件),这个查询的条件就是一个限制
@Override
public DataGrid<UserInfoVo> findList(int current, int count, UserQueryDto dto) throws Exception {
// 封装分页结果对象
DataGrid<UserInfoVo> dataGrid = new DataGrid<>();
// PageHelper 启动分页: 当前页和每页的条数,会连接后续第一条 sql 自动拼接 LIMIT
PageHelper.startPage(current , count);
// 查询用户列表(条件查询, dto 中的非空字段会作为查询条件)
List<UserInfoVo> users = userMapper.selectListSelective(dto);
// PageInfo 封装分页信息 (总条数 , 总页数 , 每条数据都是users )
PageInfo<UserInfoVo> info = new PageInfo<>(users);
dataGrid.setTotal(info.getTotal());
dataGrid.setPages(info.getPages());
dataGrid.setRows(info.getList());
return dataGrid;
}
// 根据主键更新用户信息
@Override
public void updateUser(User user) throws Exception {
userMapper.updateById(user);
}
// 根据 用户的id 物理删除
@Override
public void remove(int userId) throws Exception {
userMapper.deleteById(userId);
}
}
mapper 层
// 本质上就是一个
@Mapper
public interface UserMapper {
int insert (User user);
User selectByUsername(String username);
List<UserInfoVo> selectListSelective(UserQueryDto dto);
void updateById(User user);
void deleteById(int userId);
}
- mapper配置可以直接调用接口
<?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.iweb.user.mapper.UserMapper">
<resultMap id="BaseResultMap" type="com.iweb.user.domain.User">
<id property="userId" column="user_id"/>
<result property="username" column="username"/>
<result property="password" column="password"/>
<result property="age" column="age"/>
<result property="amount" column="amount"/>
<result property="birth" column="birth"/>
<result property="createAt" column="create_at"/>
</resultMap>
<insert id="insert" useGeneratedKeys="true" keyColumn="user_id">
insert into tb_user (username, password, create_at)
values (#{username}, #{password}, #{createAt})
</insert>
<select id="selectByUsername">
select *
from tb_user
where username = #{username}
</select>
<select id="selectListSelective"
parameterType="com.iweb.user.domain.dto.UserQueryDto"
resultType="com.iweb.user.domain.vo.UserInfoVo">
select u.user_id, u.username, u.age, u.birth, u.amount, u.birth, u.create_at
from tb_user u
<where>
<trim suffixOverrides="and">
<if test="age !=null">
u.age = #{age} and
</if>
<if test="amount != null">
u.amount = #{amount} and
</if>
<if test="birth != null">
u.birth = #{birth} and
</if>
</trim>
</where>
</select>
<update id="updateById" parameterType="com.iweb.user.domain.User">
update tb_user u
<set>
<if test="age !=null">
u.age = #{age} ,
</if>
<if test="amount != null">
u.amount = #{amount} ,
</if>
<if test="birth != null">
u.birth = #{birth} ,
</if>
</set>
where u.user_id = #{userId}
</update>
<delete id="deleteById" >
delete
from tb_user
where user_id = #{userId}
</delete>
</mapper>
Spring Security
- 结构树
src
└── main
├── java
│ └── com.iweb.user
│ ├── commons
│ │ └── Constant.java # 常量类
│ ├── config
│ │ ├── AppConfig.java # 项目自定义配置类
│ │ ├── RedisConfig.java # Redis配置
│ │ └── WebConfig.java # WebMvc配置
│ ├── controller
│ │ ├── LoginController.java # 登录接口
│ │ ├── RegisterController.java# 注册接口
│ │ └── UserController.java # 用户CRUD接口
│ ├── domain
│ │ ├── dto
│ │ │ ├── UserCreateDto.java # 创建用户接收参数
│ │ │ ├── UserEditDto.java # 修改用户接收参数
│ │ │ ├── UserLoginDto.java # 登录接收参数
│ │ │ └── UserQueryDto.java # 用户查询分页条件
│ │ └── vo
│ │ ├── UserInfoVo.java # 返回前端用户信息视图对象
│ │ ├── DataGrid.java # 分页返回封装对象
│ │ └── User.java # 用户数据库实体
│ ├── exception
│ │ └── UsernamePasswordException.java # 自定义账号密码错误异常
│ ├── filter
│ │ └── AuthSessionFilter.java # Session认证过滤器
│ ├── handler
│ │ ├── AutoExceptionHandler.java
│ │ └── GlobalExceptionHandler.java # 全局异常处理器
│ ├── interceptor
│ │ └── SessionInterceptor.java # Session登录拦截器
│ ├── mapper
│ │ └── UserMapper.java # MyBatis Mapper接口
│ ├── resp
│ │ └── R.java # 统一返回结果封装
│ ├── service
│ │ ├── impl
│ │ │ └── UserServiceImpl.java # Service业务实现类
│ │ └── UserService.java # Service业务接口
│ ├── utils
│ │ └── SessionUtil.java # Session工具类
│ └── UserApplication.java # SpringBoot启动类
└── resources
├── mapper
│ └── UserMapper.xml # MyBatis SQL映射文件
└── application.yml # 项目配置文件
├── test # 单元测试目录
└── target # maven编译输出目录
在实验之前需要在MySQL中创建一个表格
CREATE TABLE tb_user (
user_id INT PRIMARY KEY auto_increment,
username VARCHAR(10),
password VARCHAR(200),
age INT ,
amount DECIMAL ,
birth DATE ,
create_at TIMESTAMP
);
-- 登录界面中的用户为了保证是唯一的,需要用到唯一约束
ALTER TABLE tb_user ADD UNIQUE (username);


浙公网安备 33010602011771号