[Spring] SpringBoot统一功能处理与图书管理系统

🌸个人主页:https://blog.csdn.net/2301_80050796?spm=1000.2115.3001.5343

🏵️热门专栏:

🧊 Java基本语法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12615970.html?spm=1001.2014.3001.5482

🍕 Collection与数据结构 (92平均质量分)https://blog.csdn.net/2301_80050796/category_12621348.html?spm=1001.2014.3001.5482

🧀线程与网络(96平均质量分) https://blog.csdn.net/2301_80050796/category_12643370.html?spm=1001.2014.3001.5482

🍭MySql数据库(93平均质量分)https://blog.csdn.net/2301_80050796/category_12629890.html?spm=1001.2014.3001.5482

🍬算法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12676091.html?spm=1001.2014.3001.5482

🍃 Spring(97平均质量分)https://blog.csdn.net/2301_80050796/category_12724152.html?spm=1001.2014.3001.5482

感谢点赞与关注~~~

目录

  • [1. 拦截器](#1. 拦截器)
    • [1.1 概念](#1.1 概念)
    • [1.2 拦截器的基本使用](#1.2 拦截器的基本使用)
    • [1.3 拦截器的执行流程](#1.3 拦截器的执行流程)
    • [1.5 设计模式: 适配器模式](#1.5 设计模式: 适配器模式)
      • [1.5.1 定义](#1.5.1 定义)
  • [2. 统一数据返回格式](#2. 统一数据返回格式)
  • [3. 统一异常处理](#3. 统一异常处理)
  • [4. 图书管理系统(应用)](#4. 图书管理系统(应用))
    • [4.1 准备数据库](#4.1 准备数据库)
    • [4.2 创建实体类](#4.2 创建实体类)
    • [4.3 用户登录接口](#4.3 用户登录接口)
    • [4.4 添加图书接口](#4.4 添加图书接口)
    • [4.5 图书列表与翻页请求](#4.5 图书列表与翻页请求)
    • [4.6 修改图书接口](#4.6 修改图书接口)
    • [4.7 删除图书接口](#4.7 删除图书接口)
    • [4.8 批量删除接口](#4.8 批量删除接口)
    • [4.9 强制登录](#4.9 强制登录)

1. 拦截器

1.1 概念

首先什么是拦截器?拦截器主要用来拦截用户请求,在指定方法前后,根据业务需要执行的预先设定的代码.也就是允许开发人员预先设定一些代码,在用户请求响应前后执行.也可以在用户请求之前组织其执行.

就像小区门口的保安一样,会对外面的形形色色的人进行拦截一样.

1.2 拦截器的基本使用

拦截器的使用分为两步:

  1. 定义拦截器
  2. 注册配置拦截器.

首先,我们设计一个自定义拦截器.这个自定义拦截器需要实现HandlerInterceptor接口,并重写其中的所有方法.

java 复制代码
@Component
@Slf4j
public class Interceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("在执行目标方法之前");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("在执行目标方法后");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        log.info("在视图渲染完成之后");
    }
}
  • 第一个preHandle方法就是在目标方法执行前执行的代码,如果返回true,继续执行后面的操作,如果返回false,拦截当前请求.
  • 第二个postHandle表示的是在目标方法执行后要执行的操作.
  • 第三个afterCompletion表示的是在视图渲染完成之后执行的操作.第三个方法暂时不需要了解.

定义拦截器就好像定义一个保安要做什么,比如在一些奇奇怪怪的人要进来之前,保安需要在小区门口将其拦截.在业主回家的时候,保安会放行通过,并来上一句"欢迎业主回家".

其次,我们需要注册配置拦截器.需要在一个类中实现WebMvcConfigurer接口,并重写其中的addInterceptor方法.

java 复制代码
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private Interceptor interceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(interceptor)
                .addPathPatterns("/**");
    }
}

在重写的方法中,我们需要为形式参数添加对应的拦截器,之后再在拦截器的后面配置要拦截的路径,这里的/**代表的是拦截所有路径下的所有请求.

除此之外,还有一些常见的拦截路径设置.

拦截路径 含义 举例
/* ⼀级路径 能匹配/user,/book,/login,不能匹配/user/login
/** 任意级路径 能匹配/user,/user/login,/user/reg
/book/* /book下的⼀级路径 能匹配/book/addBook,不能匹配/book/addBook/1,/book
/book/** /book下的任意级路径 能匹配/book,/book/addBook,/book/addBook/2,不能匹配/user/login

注意\] 以上拦截的规则不仅仅可以是文件路径,还可以是接口的URL. ### 1.3 拦截器的执行流程 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/d0b49cf6cd5c4dc08bae5b0855308625.png) 1. 添加拦截器后,执行Controller的方法之前,请求会先被拦截器拦截住.执行 preHandle() 方法,这个方法需要返回⼀个布尔类型的值.如果返回true,就表示放行本次操作,继续访问controller中的方法.如果返回false,则不会放行(controller中的⽅法也不会执行). 2. controller当中的方法执行完毕后,再回过来执行 postHandle() 这个方法以及afterCompletion() 方法,执行完毕之后,最终给浏览器响应数据. ### 1.5 设计模式: 适配器模式 上面拦截器的实现原理中,源码中的HandlerAdapter在SpringMVC中使用的就是适配器模式. #### 1.5.1 定义 适配器模式,也叫包装器模式,是将一个类的接口,转换为客户期望的接口,适配器让原本不兼容的类之间可以合作. 比如下面两个接口,本身就是不兼容的(比如参数类型不一样,参数个数不一样等等) ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/ee0f3893a0634e259c04178268a8ee9c.png) 但是我们可以通过适配器使其兼容. ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/2be7c6be62b94567863c99311ec0b202.png) 在我们日常生活中,适配器的例子也非常常见.比如转换插头:这是我们出国旅行前必备的一个装备. ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/acda34fcf8d94390a2ffee03cbafbbb1.png) 适配器模式的角色: • Target:目标接口(可以是抽象类或接口),**客户希望直接用的接口** • Adaptee:适配者,但是与Target不兼容 • Adapter:适配器类,此模式的核心.**通过继承或者引用适配者的对象,把适配者转为目标接口** • client:需要使用适配器的对象 场景: 我们在前面学习日志的时候,slf4j就使用了适配器模式,slf4j提供的一系列打印日志的API,都是底层调用log4j或者是logback来实现的.我们在调用的时候,只需要调用slf4j的API即可. ```java //slf4j的接口 public interface Slf4j { void print(String s); } //底层打印日志的log4j public class Log4j { public void print(String s){ System.out.println(s); } } //适配器 public class Slf4jLog4jAdaptor implements Slf4j{ private Log4j log4j; public Slf4jLog4jAdaptor(Log4j log4j) { this.log4j = log4j; } @Override public void print(String s) { log4j.print(s);//最核心的是这一步,调用log4j的打印方法, //在外部调用的时候,调用的是适配器的print方法. } public static void main(String[] args) { Slf4j slf4j = new Slf4jLog4jAdaptor(new Log4j());//调用方->适配器->底层log4j slf4j.print("打印日志"); } } ``` **适配器模式的应用场景** : ⼀般来说,适配器模式可以看作⼀种"补偿模式",用来补救设计上的缺陷.应用这种模式算是"无奈之举",如果在设计初期,我们就能协调规避接口不兼容的问题,就不需要使用适配器模式了. ## 2. 统一数据返回格式 在一些场景下,我们接口的返回结果的格式是一样的,这时候我们就需要对接口的返回格式的几种不同的类型进行封装,之后在统一返回数据格式中对相应结果进行一些列逻辑判断与处理,最终由统一数据格式返回决定返回该格式中那种类型的数据. 统一数据返回格式用`@ControllerAdvice`和实现`ResponseBodyAdvice`接口来完成. ```java @ControllerAdvice public class ResponseBody implements ResponseBodyAdvice { @Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { return null; } @Override public boolean supports(MethodParameter returnType, Class converterType) { return true; } } ``` * support方法: 判断是否需要执行beforeBodyWrite方法,true为执行,false不执行.**通过该方法可以选择哪些类或者那些方法的Response需要进行处理,那些不需要处理** . 我们在获取类名和方法名的时候,需要用到反射机制,从returnType中获取: ```java //获取执⾏的类 Class declaringClass = returnType.getMethod().getDeclaringClass(); //获取执⾏的⽅法 Method method = returnType.getMethod(); ``` * deforeBodyWrite方法: 对Response方法进行具体操作处理,决定统一返回结果的内容. ## 3. 统一异常处理 统一异常处理使用的是`@ControllerAdvice +@ExceptionHandler `来实现的,`@ControllerAdvice` 表示控制器通知类,`@ExceptionHandler` 是异常处理器. 具体代码如下: ```java @RestControllerAdvice public class ErrorAdvice { @ExceptionHandler public String handler(Exception e){ return e.getMessage(); } } ``` 当然,我们也可以针对不同类型的异常返回不同的结果: ```java @RestControllerAdvice public class ErrorAdvice { @ExceptionHandler public String handler(Exception e){ return e.getMessage(); } @ExceptionHandler public String handler1(NullPointerException e){ return "发生空指针异常"+e.getMessage(); } @ExceptionHandler public String handler2(IndexOutOfBoundsException e){ return "发生数组越界异常"+e.getMessage(); } } ``` \[注意\] 在异常类型进行匹配的时候,先检查带有`@ExceptionHandler`注解的有没有与之匹配的异常类型,如果没有,依次向父类检查,直到匹配到对应的父类为止. ## 4. 图书管理系统(应用) ### 4.1 准备数据库 首先,我们需要为这个系统准备数据库和数据表,并为数据库创建一些数据. 数据库表的设计与业务逻辑有关系,数据库表一般分为两种:实体表和关系表. 图书管理系统相对比较简单,只有两个实体:**用户和图书,而且用户和图书之间没有关联关系** . 关于表的字段的设计,也与具体的业务逻辑有关系,但是三个字段是必不可少的,创建时间,更新时间,id. 用户表有用户名,密码,删除标志,图书表有图书名称,作者,售价,图书状态(可借阅,不可借阅,删除),库存数量,出版社. * 创建数据库表,并添加对应数据 ```sql DROP DATABASE IF EXISTS book_test; CREATE DATABASE book_test DEFAULT CHARACTER SET utf8mb4; -- ⽤⼾表 DROP TABLE IF EXISTS user_info; CREATE TABLE user_info ( `id` INT NOT NULL AUTO_INCREMENT, `user_name` VARCHAR ( 128 ) NOT NULL, `password` VARCHAR ( 128 ) NOT NULL, `delete_flag` TINYINT ( 4 ) NULL DEFAULT 0, `create_time` DATETIME DEFAULT now(), `update_time` DATETIME DEFAULT now() ON UPDATE now(), PRIMARY KEY ( `id` ), UNIQUE INDEX `user_name_UNIQUE` ( `user_name` ASC )) ENGINE = INNODB DEFAULT CHARACTER SET = utf8mb4 COMMENT = '⽤⼾表'; -- 图书表 DROP TABLE IF EXISTS book_info; CREATE TABLE `book_info` ( `id` INT ( 11 ) NOT NULL AUTO_INCREMENT, `book_name` VARCHAR ( 127 ) NOT NULL, `author` VARCHAR ( 127 ) NOT NULL, `count` INT ( 11 ) NOT NULL, `price` DECIMAL (7,2 ) NOT NULL, `publish` VARCHAR ( 256 ) NOT NULL, `status` TINYINT ( 4 ) DEFAULT 1 COMMENT '0-⽆效, 1-正常, 2-不允许借阅', `create_time` DATETIME DEFAULT now(), `update_time` DATETIME DEFAULT now() ON UPDATE now(), PRIMARY KEY ( `id` ) ) ENGINE = INNODB DEFAULT CHARSET = utf8mb4; -- 初始化数据 INSERT INTO user_info ( user_name, PASSWORD ) VALUES ( "admin", "admin" ); INSERT INTO user_info ( user_name, PASSWORD ) VALUES ( "zhangsan", "123456" ); -- 初始化图书数据 INSERT INTO `book_info` (book_name,author,count, price, publish) VALUES ('活 着', '余华', 29, 22.00, '北京⽂艺出版社'); INSERT INTO `book_info` (book_name,author,count, price, publish) VALUES ('平凡的 世界', '路遥', 5, 98.56, '北京⼗⽉⽂艺出版社'); INSERT INTO `book_info` (book_name,author,count, price, publish) VALUES ('三 体', '刘慈欣', 9, 102.67, '重庆出版社'); INSERT INTO `book_info` (book_name,author,count, price, publish) VALUES ('⾦字塔 原理', '⻨肯锡', 16, 178.00, '⺠主与建设出版社'); ``` * 创建项目,引入MySql驱动依赖和MyBatis依赖. ```xml org.mybatis.spring.boot mybatis-spring-boot-starter 3.0.3 com.mysql mysql-connector-j runtime ``` * 配置数据库信息,数据库日志打印,驼峰转换,MyBatis xml文件路径. ```yml spring: application: name: books # 数据库连接配置 datasource: url: jdbc:mysql://127.0.0.1:3306/books?characterEncoding=utf8&useSSL=false username: root password: qwe123524 driver-class-name: com.mysql.cj.jdbc.Driver # 配置 mybatis xml 的⽂件路径,在 resources/mapper 创建所有表的 xml ⽂件 mybatis: mapper-locations: classpath:mapper/**Mapper.xml configuration: map-underscore-to-camel-case: true #配置驼峰⾃动转换 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl ``` ### 4.2 创建实体类 实体类分为两种,一种是用户,一种是图书.定义类属性的时候,注意把数据库字段的蛇形命名法转换为小驼峰命名法. * 用户类 ```java @Data public class UserInfo { public Integer id; public String userName; public String password; public Integer deleteFlag; public Date createTime; public Date updateTime; } ``` * 图书类 ```java @Data public class BookInfo { public Integer id; public String bookName; public String author; public Integer count; public BigDecimal price; public String publish; //0-无效,1-正常,2-不可借阅 public Integer status; public String statusCN; public Date createTime; public Date updateTime; } ``` ### 4.3 用户登录接口 * 接口文档 [请求] /user/login Content-Type: application/x-www-form-urlencoded; charset=UTF-8 [参数] name=zhangsan&password=123456 [响应] true //账号密码验证正确, 否则返回false 浏览器给服务器发送 /user/login 这样的HTTP请求,服务器给浏览器返回了⼀个Boolean类型的数据.返回true,表示账号密码验证正确. * Controller层 ```java @RestController @RequestMapping("/user") public class UserInfoController { @Autowired private UserInfoService userInfoService; @RequestMapping("/login") public Boolean login(String name, String password, HttpSession session){ //用户名或者密码为空 if (!StringUtils.hasLength(name) || !StringUtils.hasLength(password)){ return false; } UserInfo userInfo = userInfoService.selectUserByName(name); //没有查询到用户 if (userInfo == null){ return false; } if (userInfo != null && password.equals(userInfo.getPassword())){ userInfo.setPassword("");//先把密码设置为空,以免被抓包 //把用户信息存入session中,session中是以json的格式存储的对象 session.setAttribute(Constant.SESSION_USER_KEY,userInfo); return true; } //密码错误 return false; } } ``` ```java public class Constant { public static final String SESSION_USER_KEY = "SESSION_USER_KEY"; } ``` 首先,确保用户输入了用户名和密码,首先判断用户名和密码不为空,如果为空,返回false. 之后,把用户输入的用户名传给Service层,返回一个用户类. 判断返回的用户类是否为null,如果为null,说明该用户不存在,返回false.如果不为null,说明用户存在,用户名存在. 用户存在之后,再判断用户输入的密码与数据库返回的用户密码是否一致,如果一致,把用户的密码设置为空字符串,再在session中存储用户信息.并返回true.否则返回false. 在设置session的时候,**我们建议把session键值对的key拿出来,作为一个静态变量独立封装**. * Service层 ```java @Service public class UserInfoService { @Autowired public UserInfoMapper userInfoMapper; public UserInfo selectUserByName(String name){ return userInfoMapper.selectUserInfo(name); } } ``` 在业务逻辑层,我们把Controller层的用户名传给Mapper层(数据库层),之后返回数据库层返回的用户信息. * Mapper层 ```java @Mapper public interface UserInfoMapper { @Select("select * from user_info where user_name = #{name} and delete_flag = 0") public UserInfo selectUserInfo(String name); } ``` 在Mapper层,我们根据Service层传过来的用户名在数据库中搜索用户,并返回用户类.**需要注意的是,需要限定有效的用户,已经删除的用户不包含在内**. * 测试接口:data返回true ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/c7cce66e4d6246bba18814049d86e202.png) ### 4.4 添加图书接口 * 接口文档 [请求] /book/addBook Content-Type: application/x-www-form-urlencoded; charset=UTF-8 [参数] bookName=图书1&author=作者1&count=23&price=36&publish=出版社1&status=1 [响应] true or false * Controller层 ```java @Autowired private BookInfoService bookInfoService; @RequestMapping("/addBook") public Boolean addBook(BookInfo bookInfo){ //有的图书信息为空 if (!StringUtils.hasLength(bookInfo.getBookName()) || !StringUtils.hasLength(bookInfo.getAuthor()) || bookInfo.getCount() == null || bookInfo.getPrice() == null || !StringUtils.hasLength(bookInfo.getPublish()) || bookInfo.getStatus() == null){ return false; } if (bookInfoService.addBook(bookInfo) == false){ log.warn("添加图书失败"); return false; } log.info("添加图书成功"); return true; } ``` 首先判断用户在添加图书的时候,图书信息输入全部合法,即不可以有任何一栏是空数据.否则返回false. 之后把图书数据传给Service层,根据Service层返回来的结果做出最终的日志打印和返回结果. * Service层 ```java public Boolean addBook(BookInfo bookInfo){ Integer i = bookInfoMapper.insertBook(bookInfo); if (i == 0) return false; return true; } ``` 调用Mapper层,把图书信息传递给Mapper层. 之后看Mapper返回的结果,如果返回的结果是0,说明数据库没有数据改变,插入失败,如果返回结果大于0,说明插入成功,返回true. * Mapper层 ```java @Insert("insert into book_info " + "(book_name, author, count, price, publish,status) " + "values (#{bookName},#{author},#{count},#{price},#{publish},#{status})") Integer insertBook(BookInfo bookInfo); ``` 从形式参数的对象中获取对应的参数,插入数据库中. * 测试 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/2eebbb11a6f541a1a1609f58ed3a252a.png) ### 4.5 图书列表与翻页请求 * 需求分析 如果数据库中的图书有成百上千条,我们不可能在一个页面中展示所有的图书,我们需要对图书进行分页. 比如我们一页展示10条数据.如果还想看其他数据,可以通过点击页码进行查询. 分页的时候,数据是这样展示的: 第一页:显示1-10条数据 第二页:显示11-20条数据 第三页:显示21-30条数据 ... 首先我们需要有更多的数据,我们在数据库中添加更多的数据. ```sql INSERT INTO `book_info` ( book_name, author, count, price, publish ) VALUES ( '图书2', '作者2', 29, 22.00, '出版社2' ),( '图书3', '作者2', 29, 22.00, '出版社 3' ), ( '图书4', '作者2', 29, 22.00, '出版社1' ),( '图书5', '作者2', 29, 22.00, '出版社 1' ), ( '图书6', '作者2', 29, 22.00, '出版社1' ),( '图书7', '作者2', 29, 22.00, '出版社 1' ), ( '图书8', '作者2', 29, 22.00, '出版社1' ),( '图书9', '作者2', 29, 22.00, '出版社 1' ), ( '图书10', '作者2', 29, 22.00, '出版社1'),( '图书11', '作者2', 29, 22.00, '出版 社1'), ( '图书12', '作者2', 29, 22.00, '出版社1'),( '图书13', '作者2', 29, 22.00, '出版 社1'), ( '图书14', '作者2', 29, 22.00, '出版社1'),( '图书15', '作者2', 29, 22.00, '出版 社1'), ( '图书16', '作者2', 29, 22.00, '出版社1'),( '图书17', '作者2', 29, 22.00, '出版 社1'), ( '图书18', '作者2', 29, 22.00, '出版社1'),( '图书19', '作者2', 29, 22.00, '出版 社1'), ( '图书20', '作者2', 29, 22.00, '出版社1'),( '图书21', '作者2', 29, 22.00, '出版 社1'); ``` 前端在发起查询的时候,需要向服务端传递的参数:首先是currentPage 当前页码,默认值为1.其次是PageSize,每页显示的条数,默认值为10. 后端响应时,需要响应给前端的数据,records所查询到的数据列表(存储到List集合中).total总记录数(用于告诉前端显示多少页,显示页数为:(total + pageSize -1)/pageSize.其次后端需要计算出每一页的图书id的看开始索引,公式为:(currentPage-1) \* pageSize. * 返回model 首先是翻页请求对象: 传入的是当前页码和每一页中展示图书的数目. ```java @Data public class PageRequest { private int currentPage = 1; // 当前⻚ private int pageSize = 10; // 每⻚中的记录数 } ``` 其次根据当前页码和每页要显示的页数,计算每页开始的索引. ```java @Data public class PageRequest { public Integer currentPage = 1; public Integer pageSize = 10; public Integer getOffset(){ return (currentPage-1)*pageSize; } } ``` 其次是翻页图书列表,用作返回结果: ```java @Data public class PageResult{ public Integer total;//图书总数 public List records;//每页的图书列表 } ``` 在每一页的返回结果中,列表中的参数类型使用泛型. * 接口文档 [请求] /book/getListByPage?currentPage=1&pageSize=10 Content-Type: application/x-www-form-urlencoded; charset=UTF-8 [参数] currentPage=1&pageSize=10 [响应] Content-Type: application/json { "total": 25, "records": [{ "id": 25, "bookName": "图书21", "author": "作者2", "count": 29, "price": 22.00, "publish": "出版社1", "status": 1, "statusCN": "可借阅" }, { ...... } ] } > 第一页可以不传参数,因为有默认值,为1. * Controller层 ```java @RequestMapping("/getListByPage") public PageResult getListByPage(PageRequest pageRequest){ log.info("获取图书列表,{}",pageRequest); return bookInfoService.getBookListByPage(pageRequest); } ``` 拿到前端传过来的参数,一个是当前页面,一个是每页的数目,把参数传递给后端,后端拿到参数之后,把参数传递给业务逻辑层,并打印日志.返回的是我们提前封装好的翻页图书列表类. * Service层 ```java public PageResult getBookListByPage(PageRequest pageRequest){ PageResult bookList = new PageResult<>(); Integer total = bookInfoMapper.count(); bookList.total = total; //为数据库的图书设置图书状态 List list = bookInfoMapper.selectBookByPage(pageRequest.getOffset(),pageRequest.getPageSize()); for (BookInfo bookInfo:list){ BookStatus nameByCode = BookStatus.getNameByCode(bookInfo.getStatus()); if (nameByCode == null){ throw new NullPointerException("book status is unknown"); } bookInfo.setStatusCN(nameByCode.getStatus()); } bookList.records = list; return bookList; } ``` 创建一个PageResult类型,之后在对象中添加图书总数.之后从对象中获取到每一页开始的索引和每一页展示的个数.获取到图书列表之后,设置图书字符串,字符串是根据每一本书的状态码来设置的.如果状态码是错误的,我们就抛出异常. 针对图书的状态,我们可以**自定义一个枚举类来解决**.设置完图书的状态信息之后,我们就需要把图书列表给bookList中的records信息. ```java @AllArgsConstructor public enum BookStatus { DELETE(0,"无效"), NORMAL(1,"正常"), FORBIDDEN(2,"不可借阅"); @Getter private Integer code; @Getter private String status; /** * 根据图书的状态码获取枚举对象 * @param code 图书状态码 * @return 返回枚举对象 */ public static BookStatus getNameByCode(Integer code){ for (BookStatus bookStatus:BookStatus.values()){//遍历所有枚举对象 if (bookStatus.getCode().equals(code)){ return bookStatus; } } return null; } } ``` 在枚举类中,我们首先要定义枚举对象的属性,一个是状态码(code),一个是状态信息(msg).之后是枚举对象,枚举对象中需要传入枚举对象的属性. 之后我们需要把图书的状态码和枚举对象的状态码对上,我们可以使用`BookStatus.values()`获取到枚举类中的所有枚举对象,之后进行遍历,只要枚举对象的状态码和图书的状态码对得上,就返回对应枚举对象中的信息.否则返回null. * Mapper层 ```java @Select("select count(*) from book_info where status != 0") Integer count(); @Select("select * from book_info where status != 0 order by id desc " + "limit #{pageSize} offset #{offset}") List selectBookByPage(Integer offset,Integer pageSize); ``` Mapper层要做的是,查询图书的总数,根据页码和每一页显示的图书本数,返回查询的图书列表. * 测试 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/3e5d61d075e145db9ec359b0284a9406.png) ### 4.6 修改图书接口 * 接口文档 [请求] /book/queryBookById?bookId=25 [参数] 无 [响应] { "id": 25, "bookName": "图书21", "author": "作者2", "count": 999, "price": 222.00, "publish": "出版社1", "status": 2, "statusCN": null, "createTime": "2023-09-04T04:01:27.000+00:00", "updateTime": "2023-09-05T03:37:03.000+00:00" } 首先我们需要根据图书ID,获取到当前图书的信息. 点击修改按钮之后,修改图书信息. [请求] /book/updateBook Content-Type: application/x-www-form-urlencoded; charset=UTF-8 [参数] id=1&bookName=图书1&author=作者1&count=23&price=36&publish=出版社1&status=1 [响应] true or false 我们约定,浏览器给服务器发送⼀个 /book/updateBook 这样的HTTP请求, form表单的形式来提交数据. 这里的参数就和添加图书是一样的. * Controller层 ```java @RequestMapping("/queryBookById") public BookInfo getBookById(Integer bookId){ BookInfo bookInfo = bookInfoService.getBookById(bookId); if (bookInfo == null){ log.warn("图书Id有误"); return new BookInfo(); } return bookInfo; } @RequestMapping("/updateBook") public Boolean updateBook(BookInfo bookInfo){ if (bookInfo.getId() == null){//在根据如数id查询图书的时候,有可能返回的是空对象 log.error("图书Id不能为空"); return false; } Integer ret = bookInfoService.updateBook(bookInfo); if (ret > 0){ return true; } log.warn("添加图书失败"); return false; } ``` 首先根据传入的图书id来查询到图书的信息.如果根据id没有查询到图书信息,则返回空图书类型.查询到则返回查询结果. 之后更新图书信息的时候,如果图书id为空的时候,就返回false,之后把更新后的图书对象传给Service层,如果返回的类型是大于0,则说明更新成功,否则更新失败. * Service层 ```java public BookInfo getBookById(Integer bookId){ return bookInfoMapper.selectBookById(bookId); } public Integer updateBook(BookInfo bookInfo){ return bookInfoMapper.updateBook(bookInfo); } ``` 查询图书的时候,只需要把图书id传给Mapper层即可.更新图书一样,也是直接把图书对象传给Mapper层即可. * Mapper层 ```java @Select("select * from book_info where id = #{bookId}") BookInfo selectBookById(Integer bookId); Integer updateBook(BookInfo bookInfo); ``` 查询图书的时候,我们使用注解来完成sql语句,但是更新图书的时候,只有id是必备选项,而其他属性都可以选择性更新.所以我们使用xml来完成sql.我们在这里需要使用到标签. ```xml update book_info book_name = #{bookName} author = #{author} count = #{count} price = #{price} publish = #{publish} status = #{status} where id = #{id} ``` 在传入的参数给对象中的属性赋值的时候,即对象拥有该属性的时候,就更新该字段,否则不更新. ### 4.7 删除图书接口 我们前面提到,删除分为逻辑删除和物理删除.我们这里使用的是逻辑删除.这里的删除就是把图书的状态(status)修改一下. * 接口文档 [请求] /book/updateBook Content-Type: application/x-www-form-urlencoded; charset=UTF-8 [参数] id=1&status=0 [响应] true or false 由于这里的逻辑和更新图书的逻辑完全一样,我们不再赘述. ### 4.8 批量删除接口 批量删除图书,其实就是批量修改图书的状态. * 接口文档 [请求] /book/batchDeleteBook Content-Type: application/x-www-form-urlencoded; charset=UTF-8 [参数] [响应] true or false 点击\[批量删除\]按钮时,只需要把复选框选中的图书的ID,发送到后端即可.多个id,我们使用List的形式来传递参数 * Controller层 ```java @RequestMapping("/batchDeleteBook") public Boolean beachDeleteBook(List ids){ if (ids == null || ids.isEmpty()){ return true;//没有要删除的图书 } Integer ret = bookInfoService.beachDeleteBooks(ids); if (ret == 0){ log.error("批量删除失败"); return false; } return true; } ``` 传入需要删除的图书id,之后判断ids是否为null或者ids为空,说明没有要删除的图书,返回true.之后把ids传给Service层,要是返回结果是0,说名没有图书被删除,返回false.否者返回true. * Service层 ```java public Integer beachDeleteBooks(List ids){ return bookInfoMapper.beachDeleteBooks(ids); } ``` 直接把id传给Mapper层 * Mapper层 Mapper层我们依然使用xml的方式实现.因为需要遍历list,我们需要使用``标签. ```java Integer beachDeleteBooks(List ids); ``` ```xml update book_info set status = 0 where id in #{id} ``` ### 4.9 强制登录 虽然我们做了用户登录,但是我们发现,用户不登录,依然可以操作图书. 这是有极大风险的.所以我们需要进行强制登录. 如果用户未登录就访问图书列表或者添加图书等页面,强制跳转到登录页面. * 实现思路 在用户登录的时候,用户把用户的信息存储在了session中,这时候我们**使用统一功能中的拦截器,对除了登录页面的接口统一拦截.判断session中是否存在用户的信息**.存在就说明用户已经登录,没有就说明没用登录. * 返回结果封装 我们不妨对所有后端的数据进行封装为Result.并添加对不同的状态的方法,方法中对Result的属性进行设置. ```java @Data public class Results { //200-成功 -1-用户未登录 -2后端结果错误 public Status code;//状态码 public String msg;//状态信息 public T data;//返回页面的数据 public static Results success(T data){ Results results = new Results<>(); results.setCode(Status.SUCCESS); results.setMsg("正常"); results.setData(data); return results; } public static Results noLogin(){ Results results = new Results<>(); results.setCode(Status.NOLOGIN); results.setMsg("用户未登录"); return results; } public static Results error(String s){ Results results = new Results<>(); results.setCode(Status.ERROR); results.setMsg("内部错误" + s); return results; } } ``` 其中Status是后端逻辑处理的状态码,我们可以通过一个枚举类来封装. ```java @AllArgsConstructor public enum Status { SUCCESS(200,"成功"), NOLOGIN(-1,"用户未登录"), ERROR(-2,"后端返回结果错误"); @Getter private Integer code; @Getter private String msg; } ``` * 定义拦截器 ```java /** * 登录拦截器 */ @Slf4j @Component public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(false); if (session == null || session.getAttribute(Constant.SESSION_USER_KEY) == null){ log.error("用户未登录"); response.getOutputStream().write(Results.noLogin().getMsg().getBytes(StandardCharsets.UTF_8)); response.setStatus(401); return false; } return true; } } ``` 实现`HandlerInterceptor`接口,重写`preHandle`方法. 首先从request中拿到session,**其中参数要设置为false** ,防止session不存在自动创建.从session中获取用户信息,用户信息存储在`SESSION_USER_KEY`中,如果该字段的信息为空,或者是session不存在的时候,说明没有登录,返回false进行拦截.设置Response的状态码为401,在Response的字节流中输入错误信息.该错误信息由Result类中的nologin方法生成. 如果获取到了用户信息,则放行. * 注册拦截器 ```java @Configuration public class WebConfig implements WebMvcConfigurer { @Autowired private LoginInterceptor loginInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns("/**") .excludePathPatterns("/user/login") .excludePathPatterns("/css/**") .excludePathPatterns("/js/**") .excludePathPatterns("/pic/**") .excludePathPatterns("**/*.html"); } } ``` 实现`WebMvcConfigurer`接口,并重写`addInterceptors`方法,之后在形式参数中,使用`addInterceptor`方法添加拦截器,并使用`excludePathPatterns`和`addPathPatterns`对该拦截器的进行配置.拦截的可以是url,也可以是文件路径.不拦截登录页面和前端的所有页面. * 统一返回结果 ```java @RestControllerAdvice @Slf4j public class ResponseAdvice implements ResponseBodyAdvice { @Autowired private ObjectMapper mapper; @SneakyThrows @Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { //如果返回结果是String类型的,需要转换为json格式 if (body instanceof String){ return mapper.writeValueAsString(Results.success(body)); } //返回结果是Results类型,则不需要对结果进行封装 if (body instanceof Results){ return body; } return Results.success(body); } @Override public boolean supports(MethodParameter returnType, Class converterType) { return true; } } ``` 实现`ResponseBodyAdvice`接口,重写`beforeBodyWrite`方法. 这里我们需要注意的是对**返回Body是字符串类型的需要进行特殊处理,需要把返回结果转换为json格式**.要是返回的结果就是Result类型,直接返回body,如果是其他的类型,需要body封装进Result类型中,并调用success方法.

相关推荐
冷琅辞23 分钟前
Go语言的嵌入式网络
开发语言·后端·golang
response_L24 分钟前
国产系统统信uos和麒麟v10在线打开word给表格赋值
java·c#·word·信创·在线编辑
苹果酱056725 分钟前
Golang标准库——runtime
java·vue.js·spring boot·mysql·课程设计
User_芊芊君子30 分钟前
【Java】类和对象
java·开发语言
martian6651 小时前
Spring Boot后端开发全攻略:核心概念与实战指南
java·开发语言·spring boot
跟着珅聪学java3 小时前
spring boot +Elment UI 上传文件教程
java·spring boot·后端·ui·elementui·vue
我命由我123453 小时前
Spring Boot 自定义日志打印(日志级别、logback-spring.xml 文件、自定义日志打印解读)
java·开发语言·jvm·spring boot·spring·java-ee·logback
lilye663 小时前
程序化广告行业(55/89):DMP与DSP对接及数据统计原理剖析
java·服务器·前端
徐小黑ACG4 小时前
GO语言 使用protobuf
开发语言·后端·golang·protobuf
战族狼魂6 小时前
CSGO 皮肤交易平台后端 (Spring Boot) 代码结构与示例
java·spring boot·后端