SpringBoot学习_day3

Spring Boot整合druid

整合druid的步骤主要为:

  • 导入依赖druid-spring-boot-starter,mysql-connector-java依赖
  • 在在配置文件中配置数据库驱动
  • 就可以进行测试了

Spring Boot整合Mybatis-plus

Spring Boot整合Mybatis-Plus的基本步骤为:

  • 导入依赖Mybatis-Plus-boot-starter,因为需要利用数据库,所以还需要导入druid数据库源以及connector-java依赖,如下所示:

    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.4.2</version>
    </dependency>
    
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.1.10</version>
    </dependency>
    
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    
  • 因为利用到了数据库,所以在配置文件中需要配置数据库驱动

    spring:
      datasource:
        druid:
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://localhost:3306/ssmp?serverTimezone=Asia/Shanghai
          username: root
          password: root
    
  • 对应的mapper接口继承了BaseMapper<T>,这样这个mapper接口可以不需要再写任何的代码,然后可以执行对应的操作了,而如果利用的是mybatis的时候,需要再方法上面利用对应的注解来执行响应的操作,或者需要通过映射文件,创建代理对象来操作。如下所示:

    /*
    通过mybatis-plus来实现的时候,它是通过定义mapper,然后
    让这个mapper接口来继承BaseMapper<T>,然后什么代码都不需要
    写,因为BaseMapper中已经定义了各种操作数据库的方法了,然后
    查询到的数据返回的就是T。
     */
    @Mapper
    public interface BookDao extends BaseMapper<Book> {
    }
    

    BaseMapper<T>的部分源码如下所示:

    public interface BaseMapper<T> extends Mapper<T> {
        int insert(T entity);//插入操作,如果没有配置,那么id并不支持自增操作,所以需要配置文件中进行配置
        int deleteById(Serializable id);//根据id进行删除操作
        int updateById(@Param("et") T entity);//根据id进行编辑
        int update(@Param("et") T entity, @Param("ew") Wrapper<T> updateWrapper);
        T selectById(Serializable id);//根据id来查询
        T selectOne(@Param("ew") Wrapper<T> queryWrapper);//查询单个数据
        Integer selectCount(@Param("ew") Wrapper<T> queryWrapper);
        List<T> selectList(@Param("ew") Wrapper<T> queryWrapper);//查询多条数据,如果queryWrraper不为null,那么就是根据这个参数进行条件查询
    }
    
    
  • 进行测试

但是进行测试查询等操作的时候,发现数据库表找不到,这是因为mybatis-plus中默认是根据实体类的名字作为数据库表名,而我们的数据库表的名字则是在实体名的前面添加了tb_前缀,从而发生数据库表找不到。为了解决这个问题,需要在配置文件中设置数据库表的前缀。

mybatis-plus:
  global-config:
    db-config:
      table-prefix: tb_ # 配置数据库表的前缀

但是如果执行插入操作的时候,发现了报错,提示Could not set property 'id' of 'class com.example.domain.Book' with value '1557236544425811970',Cause: java.lang.IllegalArgumentException: argument type mismatch,因为mybatis-plus中id默认并不是自增操作的,所以我们同样需要在配置文件中配置id-Type,使其支持自增。

mybatis-plus:
  global-config:
    db-config:
      table-prefix: tb_ # 配置数据库表的前缀 
      id-type: auto # 配置id是支持自增的

但是这时候如果我们需要通过日志来调试,那么需要看到执行的sql语句,这时候我们同样需要在配置文件中进行配置,使得它是标准输出,如下所示:

mybatis-plus:
  global-config:
    db-config:
      table-prefix: tb_ # 配置数据库表的前缀
      id-type: auto # 配置id是支持自增的
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 配置日志输出为标准输出,方便看到执行的sql语句

mybatis-plus实现分页操作时,那么这时候需要通过调用selectPage(IPage,QueryWrapper),这时候第一个参数设置查询的是第几页的记录,QueryWrapper封装的是条件,如果不为null,那么就是在条件查询的基础上,进行分页操作。

当调用selctPage操作之后,返回值是一个IPage对象,也可以用传递的参数IPage来存放数据。这时候通过这个对象调用以下方法来获取对应的信息:

  • getPages() : 获取总页数
  • getTotal(): 获取总记录数
  • getCurrent(): 获取当前时第几页
  • getSize(): 获取每一页有多少行
  • getRecords(): 获取当前页的所有记录

所以对应的代码为:

测试类:

package com.example.domain;

public class Book {
    private Integer id;
    private String name;
    private String description;

    public Book() {
    }

    public Book(Integer id, String name, String description) {
        this.id = id;
        this.name = name;
        this.description = description;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    @Override
    public String toString() {
        return "Book{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", description='" + description + '\'' +
                '}';
    }
}

@SpringBootTest
public class BookDaoTest {
    @Autowired
    private BookDao bookDao;
    /*
    利用mybatis-plus来是实现分页操作,首先需要
    创建MybatisPlusInterceptor,在这个拦截器
    中添加一个内部拦截器PageInnerInterceptor,用于分页操作的。
    如果没有这一步,那么执行下面代码的的时候,sql语句不会出现limit子句。
    完成上面的操作之后,通过bookDao调用selectPage方法来执行分页操作,但是
    我们需要知道要查询的是第几页,并且每一页由多少条记录。所以需要传递参数
    IPage(他是一个接口),它已经封装了查询了第几页,每一页由多少条记录。
    selectPage(IPage,QueryWrapper<T>),其中第二个参数用于条件查询的。
     */
    @Test
    public void testPage(){
        IPage page = new Page(2,5);//获取第1页的数据,每一页有5行
        bookDao.selectPage(page,null);//将执行selectPage操作,将查询到的数据存放到page中,第二个参数表示的是条件
        System.out.println("当前是第 " + page.getCurrent() + " 页");
        System.out.println("每一页有 " + page.getSize() + " 行");
        System.out.println("总共有 " + page.getTotal() + " 行,总页数有 " + page.getPages() + " 页");
        System.out.println("当前页的记录分别为: ");
        List<Book> records = page.getRecords();
        for(Book record : records){
            System.out.println(record);
        }
    }
 
}

但是运行的时候,就会通过日志看到,并没有看到LIMIT子句,所以查询的就是所有的数据:
在这里插入图片描述

所以在进行分页操作之前,首先我们需要添加一个MyBatisPlus拦截器,然后再这个拦截器的内部添加一个PaginationInnerInterceptor进行拦截操作,从而实现分页操作,对应的代码为:

@Configuration //Mybatis-Plus的配置类
public class MPConfig {
    @Bean //将方法返回值添加到IOC容器中
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
        //mybatis-plus添加拦截器
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        //拦截器内部再次添加拦截器,用于分页操作,所以需要添加分页操作的拦截器
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return interceptor;
    }
}

再次执行测试类的方法时,日志输出的sql语句就含有limit字句了,如下所示:
在这里插入图片描述

通过上面可以知道,如果需要实现条件查询,那么这时候我们再调用对应的方法的时候,传递参数QueryWrapper或者LambdaQueryWrapper对象即可,通过这个对象调用对应的方法来设置条件,代码如下所示:

    /*
    测试Mybatis-plus的条件查询,只需要添加QueryWrapper参数即可,也可以是
    LambdaWrapper参数,然后通过这个对象调用对应的方法来设置对应的条件。
     */
    @Test
    public void testGetBy(){
        //查询名字为book888,并且description为玄幻小说的书
        QueryWrapper<Book> qw = new QueryWrapper<>();
        qw.eq("name","book888");//第一个参数表示数据库表中的字段名
        qw.eq("description","玄幻小说");
        Book book = bookDao.selectOne(qw);
        System.out.println(book);
    }
    /*
    在条件查询中,如果利用的是参数QueryWrapper来设置条件,那么需要
    注意第一个参数数据库的字段名没有写错。所以这时候为了防止写错
    的情况,所以就利用LambdaQueryWrapper,通过lambda表达式来获取字段名即可
     */
    @Test
    public void testGetBy2(){
        LambdaQueryWrapper<Book> lqw = new LambdaQueryWrapper();
        /*
        lqw.eq(Book::getDescription,"玄幻小说");//获取description字段值为玄幻小说的Book
        但是这样写有弊端,这样通过设置日志之后,来看到它的sql语句,发现如果传递的参数是null
        也即lqw.eq(Book::getDescription, null)的时候,那么这时候执行的sql语句是查询
        description字段中为null的Book。也即null变成了它的值。
        所以为了避免这种情况的发生,我们需要定义一个变量来定义条件.
        第一种做法为:在执行这个操作之前,判断这个变量是否为null即可.
        第二种做法:因为queryWrapper对象调用对应的方法设置条件的时候,在字段名
        前面还有一个boolean参数,如果是true,那么添加这个条件,否则不添加。
        所以我们根据字段名是否为null即可。
        */
        String description = null;
        lqw.eq(description != null,Book::getDescription,description);//变量description不为null时添加条件
        List<Book> books = bookDao.selectList(lqw);

        for(Book book : books){
            System.out.println(book);
        }
    }

但是建议传递的参数是LambdaQueryWrapper对象,这是因为QueryWrapper对象调用对应的方法来设置条件的时候,方法中字段名必须要和数据库表的一致,很容易写错,而在LambdaQueryWrapper对象中,通过实体来获取对应的属性,就可以获取到条件对应的值了。这是因为实体类中的属性必然和数据库表的字段名相同,所以安全性更加高。

值得一提的是,如果第二个参数的值为null的话,也即lqw.eq(Book::getDescription,description)中的description为null的话,那么在日志中可以看到,他查询的是数据库表中description字段值为"null"的数据,但是事实上我们可能没有传递这个参数,如下所示:

测试代码:

@Test
public void testGetBy(){
    //查询名字为book888,并且description为玄幻小说的书
    QueryWrapper<Book> qw = new QueryWrapper<>();
    qw.eq("name",null);//第一个参数表示数据库表中的字段名
    qw.eq("description","玄幻小说");
    Book book = bookDao.selectOne(qw);
    System.out.println(book);
}

在这里插入图片描述

所以一种做法是在设置条件之前判断变量description是否为null,如果为null,那么不会进行条件查询,否则进行条件查询。

但是还有第二种做法,LambdaQueryWrapper或者QueryWrapper对象中还有xxx(boolean,column,value)方法,其中第一个参数boolean的值如果为true,表示会设置条件进行条件查询,否则如果为false,表示不会设置条件。所以一般建议采用的是第二种方法。

代码如下所示:

@Test
    public void testGetBy(){
        //查询名字为book888,并且description为玄幻小说的书
        QueryWrapper<Book> qw = new QueryWrapper<>();
        String name = null;
        qw.eq(name != null,"name",name);//第一个参数表示数据库表中的字段名
        qw.eq("description","玄幻小说");
        List<Book> books = bookDao.selectList(qw);
        for(Book book : books) {
            System.out.println(book);
        }
    }

在这里插入图片描述

同时也可以看到,如果有多个条件的时候,那么这时候我们只需要通过QueryWrapper或者LambdaQueryWrapper对象调用多个方法来设置条件即可。

所以利用spring boot来整合第三方技术的时候,基本步骤为:导入xxx-starter依赖,如果这个依赖并没有在spring中配置,那么就需要自己手动来配置它的版本,负责不需要配置版本了。

ssmp进行简单开发

我们利用spring + spring mvc + mybatis-plus来进行开发一个简单图书管理,主要是来实现它的增删改查。

准备工作:

  • 导入相关坐标:mybatis-plus-boot-starter,druid-spring-boot-starter,mysql-connector-java,spring-boot-starter-thymeleaf.之所以需要导入thymeleaf-spring-boot-starter,是因为我们需要进行渲染,并且在获取数据之后,我们可以通过thymeleaf来进行相应的操作,例如循环遍历等,否则,如果没有导入thymeleaf的话,那么就没有办法跳转到templates目录下面的html文件中。

    <!--导入mybatis-plus以及druid坐标,
            由于spring中没有配置这些坐标,所以需要手动配置版本-->
    <!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-boot-starter -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.4.2</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/com.alibaba/druid-spring-boot-starter -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.1.10</version>
    </dependency>
    
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>org.junit.vintage</groupId>
                <artifactId>junit-vintage-engine</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    
    <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-thymeleaf -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
        <version>2.6.6</version>
    </dependency>
    
  • 配置文件中配置数据库驱动,以及mybatis-plus

  • 建立数据库表tb_book,如果直接通过mybatis-plus来实现数据库的操作,很容易发生报错,例如查询操作的时候,会发生报错,提示数据库表book不存在,这是因为我们的数据库表前面还有前缀tb_,或者执行插入操作的时候,发生错误,这是因为mybatis-plus中默认id不是支持自增的,所以我们在建立数据库表之后,还需要在配置文件中通过mybatis-plus来配置数据库表的前缀,id的类型。此外还需要配置日志,从而方便调试。

  • 创建数据库表对应的实体类,以及controller,service,dao层

    public class Book {
        private Integer id;
        private String name;
        private String description;
    
        public Book() {
        }
    
        public Book(Integer id, String name, String description) {
            this.id = id;
            this.name = name;
            this.description = description;
        }
    
        public Integer getId() {
            return id;
        }
    
        public void setId(Integer id) {
            this.id = id;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public String getDescription() {
            return description;
        }
    
        public void setDescription(String description) {
            this.description = description;
        }
    
        @Override
        public String toString() {
            return "Book{" +
                    "id=" + id +
                    ", name='" + name + '\'' +
                    ", description='" + description + '\'' +
                    '}';
        }
    }
    
    /*
    通过mybatis-plus来实现的时候,它是通过定义mapper,然后
    让这个mapper接口来继承BaseMapper<T>,然后什么代码都不需要
    写,因为BaseMapper中已经定义了各种操作数据库的方法了,然后
    查询到的数据返回的就是T。
     */
    @Mapper
    public interface BookDao extends BaseMapper<Book> {
    }
    
    /*
    在mybatis-plus中,service层的实现类也可以不写代码,
    可以通过定义一个IBookService,使得这个接口继承了IService<T>
    然后定义这个接口的实现类IBookServiceImpl,使得在实现
    这个接口的同时,继承了ServiceImpl<M,T>,其中这个M就是对应的Mapper接口,
    T就是对应的实体.
    此时的service实现类就已经有了操作的各种方法了,如果需要添加功能的
    时候,就需要手动在这个实现类中追加即可
     */
    public interface IBookService extends IService<Book> {
        IPage<Book> getPage(int currentPage, int size);
    }
    
    
    @Service
    public class IBookServiceImpl extends ServiceImpl<BookDao,Book> implements IBookService {
        //手动追加方法,实现分页操作
        @Autowired
        private BookDao bookDao;
        public IPage<Book> getPage(int currentPage, int size){
            IPage<Book> page = new Page(currentPage, size);
            bookDao.selectPage(page,null);
            return page;
        }
    }
    

在上面的操作完毕之后,可以进行开发了:

  • 查询所有的图书

    当我们查询图书的时候,将通过IBookService调用list()方法来返回一个List<Book>对象,这时候我们需要将这个数据封装到视图中,然后跳转到对应的界面。但是这时候如果数据太多,影响美观,所以需要进行分页操作,因此进入首页中查询的是第一页的数据,封装到页面中的数据是一个IPage<Book>对象。但是如果在前端中怎么显示数据呢?因为有多个数据,所以必然是要用到了循环来实现,因此需要通过thymeleaf中来进行循环操作。对应的代码为:

     @GetMapping
    //获取所有的数据,然后来到首页
    public ModelAndView index(ModelAndView modelAndView){
        //获取首页的数据,因为需要实现分页查找
        IPage<Book> page = new Page(1, 5);
        iBookService.page(page,null);
        //将r添加到前端中
        modelAndView.addObject("page",page);
        //设置跳转的页面
        modelAndView.setViewName("/pages/books.html");//html文件放到了templates/pages下面
        return modelAndView;
    }
    
  • 编辑图书

    首先当我们点击要编辑哪一本书的时候,我们需要获取这个书的信息,然后跳转到编辑页面,而在这个编辑页面中显示的是这个图书原来的信息。当编辑完毕之后,再次提交,当编辑成功之后,直接重定向到首页中。对应的代码为:

    @GetMapping("/modifyById/{id}")
    public ModelAndView modifyById(@PathVariable("id")Integer id){
        Book book = iBookService.getById(id);
        ModelAndView modelAndView = new ModelAndView();
        //设置属性
        modelAndView.addObject("book",book);
        //设置跳转的页面
        modelAndView.setViewName("/pages/update.html");
        return modelAndView;
    }
    
    @PostMapping("/modifyById")
    /*
        通过这个注解@ResponseBody,告诉spring,这个返回值并不是用来页面跳转的,而是回写数据
        如果没有这个注解,那么下面的代码就会进行页面跳转,这时候由于方法返回值是void,那么就会
        再次来到当前的页面中.
         */
    @ResponseBody
    public void modifyById(Book book,HttpServletResponse response) throws IOException { //注意的是这里并没有在Book的前面使用注解@RequestBody,否则就会发生报错
        System.out.println("编辑后的书为: " + book);
        iBookService.updateById(book);
        response.sendRedirect("/books3");//为了避免当编辑完成之后,当来到首页的时候,url依旧没有发生变化,所以需要进行重定向
    }
    

    编辑页面:

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>编辑页面</title>
    </head>
    <body>
        <form action="/books3/modifyById" method="post">
            <input type="hidden" name="id" th:value="${book.id}">
            图书名字<input type="text" name="name" th:placeholder="${book.name}"><br>
            图书描述<input type="text" name="description" th:placeholder="${book.description}"><br>
            <input type="submit" value="提交">
        </form>
    </body>
    </html>
    

    在看到上面的代码之后,可能会疑惑,为什么不可以在编辑完成之后,不可以通过注解@RequestBody来表示编辑之后的Book呢?我们尝试给这个参数前面添加这个注解,即代码如下所示:

    @ResponseBody
    public void modifyById(@RequestBody Book book,HttpServletResponse response)
    

    然后运行之后,结果如下所示:
    在这里插入图片描述

    编辑操作点击提交之后,变成了:
    在这里插入图片描述

    数据库中数据没有编辑成功,重新回到IDEA,发现控制台中打印这一串数据:

    2022-08-11 16:08:26.361  WARN 13940 --- [nio-8080-exec-5] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.HttpMediaTypeNotSupportedException:Content type 'application/x-www-form-urlencoded;charset=UTF-8' not supported]
    

    重点看Content type 'application/x-www-form-urlencoded;charset=UTF-8' not supported这句,提示application/x-www-form-urlencoed;charset=UTF-8不支持,这是因为什么呢?

    这是因为@RequestBody注解导致的。可以看一下这个文章:@RequestBody 的使用方法和注意事项,查看之后可以发现,原来是因为@RequestBody支持的是application/json格式,当发送请求之后,他会将json格式的字符串绑定到对应的bean中。而在一些表单form提交,**类型为application/x-www-form-urlencoded;charset=UTF-8**的情况下,并不能使用这个注解了。所以此时我们可以直接删除这个注解,就可以了,这就涉及到了spring中的POJO了。所以这就是为什么不可以在Book参数前面添加注解@RequestBody了.

  • 删除图书

    点击删除的超链接,当点击之后,就需要进行删除操作,删除完毕之后,需要重新重定向到首页,对应的代码如下所示:

    @GetMapping("/deleteById/{id}")
    @ResponseBody
    public void deleteById(@PathVariable("id")Integer id, HttpServletResponse response) throws IOException {
        iBookService.removeById(id);
        response.sendRedirect("/books3");
    }
    
  • 添加图书

    这个操作和编辑类似,所以代码也是相似的:

    @GetMapping("/save")
    public String save(){
        return "/pages/add.html";
    }
    @PostMapping("/save")
    @ResponseBody
    public void save(Book book, HttpServletResponse response) throws IOException {
        System.out.println("添加的图书: " + book);
        iBookService.save(book);
        //重定向到首页
        response.sendRedirect("/books3");
    }
    
  • 分页操作

    因为我们来到首页的时候,封装的数据是IPage对象,这个对象可以通过对应的方法来获取需要的信息,如下所示:

    1. getCurrent() : 获取当前页面是第几页
    2. getTotal(): 获取一共有多少行
    3. getPages(): 获取一共有多少页
    4. getSize(): 获取每一页有多少行
    5. getRecords(): 获取每一页的数据,返回的是List<T>对象。

    所以这时候我们需要通过循环来遍历这个List<T>对象,此时需要利用thymeleaf中的each用法。同时,要实现分页操作,还需要进行if判断,如果是第一页的话,那么上一页就会失效,同理,如果是最后一页,下一页就会失效。而if判断如果为true,那么修饰的标签就会显示,否则不会出现(并不是隐藏,而是真的没有这个标签)

    同时,为了点击下一页的时候,我们可以重新渲染页面,还需要再conroller中添加方法来实现分页操作,需要传递的参数是currentPage, size,表示第几页,以及每一页有多少行.

    @GetMapping("/getPage/{currentPage}/{size}")
    public ModelAndView getPage(@PathVariable("currentPage")Integer currentPage,
                                @PathVariable("size")Integer size,
                                ModelAndView modelAndView){
        //查询currentPage的记录,并且每页有size页
        IPage page = new Page(currentPage, size);
        iBookService.page(page,null);
        modelAndView.addObject("page",page);
        //设置跳转的视图
        modelAndView.setViewName("/pages/books.html");
        return modelAndView;
    }
    

books.html的代码为:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>books页面</title>
</head>
<body>
    <h1>ssmp练习</h1>
    <div>
        <a th:href="'/books3/save'">添加</a>
    </div>
    <div>
        <table>
            <thead>
               <td>图书id</td>
               <td>图书名字</td>
               <td>图书描述</td>
               <td>操作</td>
            </thead>
            <tr th:each="d:${page.records}">
                <td th:text="${d.id}"></td>
                <td th:text="${d.name}"></td>
                <td th:text="${d.description}"></td>
                <td>
                    <a th:href="'/books3/modifyById/' + ${d.id}">编辑</a>
                    <a th:href="'/books3/deleteById/' + ${d.id}">删除</a>
                </td>
            </tr>
        </table>
        <div>
            <!--实现分页操作-->
            <a th:href="'/books3/getPage/1/' + ${page.size}">首页</a>
            <a th:href="'/books3/getPage/' + ${page.current - 1} + '/' + ${page.size}" th:if="${page.current ne 1}">上一页</a>
            <a th:href="'#'" th:if="${page.current eq 1}">上一页</a>
            <a th:href="'/books3/getPage/' + ${page.current + 1} + '/' + ${page.size}" th:if="${page.current ne page.pages}">下一页</a>
            <a th:href="'#'" th:if="${page.current eq page.pages}">下一页</a>
            <a th:href="'/books3/getPage/' + ${page.pages} + '/' + ${page.size}">尾页</a>
        </div>
    </div>
</body>
</html>

前端页面如下所示(每一页有2行):
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值