【牛客讨论区】第一章:开发社区首页

1. 环境搭建

项目所需素材:
链接:https://pan.baidu.com/s/1Aqp2XWzdgAHc1t-ERW-pEQ
提取码:hjm5


在这里插入图片描述


构建Maven项目

在这里插入图片描述
在这里插入图片描述

构建完之后,项目结构如下:

在这里插入图片描述

编译项目,生成 target 目录有两种方式,1.右上角 maven–Lifecycle–compile,2.菜单栏"Build"–“Build Project”(Ctrl + F9)

构建SpringBoot项目

由于项目所需要的包太多了,如果用到哪个包,再去maven仓库复制依赖代码,添加到 pom 中,那还是太麻烦了,基于此,SpringBoot 按照功能将一些包进行了整合,比如 web 开发常用的依赖包都整合在了 SpringWeb 中,只需要引入一个 SpringWeb 的包即可,无需自己去一个个引入了

在这里插入图片描述

在这里插入图片描述

pom.xml 添加依赖

<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
    <version>2.6.3</version>
</dependency>

测试环境是否搭建成功
写个简单的 controller

package com.nowcoder.community.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping("/alpha")
public class AlphaController {

    @RequestMapping("/hello")
    @ResponseBody
    public String sayHello() {
        return "Hello, SpringBoot";
    }
}

浏览器访问 http://localhost:8080/alpha/hello,数据成功回显!

如果想要更改端口号,可以修改 resources目录下的 application.properties 文件

# 应用服务 WEB 访问端口
server.port=8080

添加项目的访问根路径

server.servlet.context-path=/community

重启项目,访问 http://localhost:8080/community/alpha/hello,依然成功!

2. Spring入门

bean 的获取

新建接口

package com.nowcoder.community.dao;

public interface AlphaDao {
    String select();
}

实现类1

package com.nowcoder.community.dao;

import org.springframework.stereotype.Repository;

@Repository("alphaHibernate") //给这个bean起个名字
public class AlphaDaoHibernateImpl implements AlphaDao{
    @Override
    public String select() {
        return "Hibernate";
    }
}

实现类2

package com.nowcoder.community.dao;

import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Repository;

@Repository
@Primary //加了这个注解,同一接口AlphaDao 有多个实现类,会优先返回这个bean
public class AlphaDaoMyBatisImpl implements AlphaDao {
    @Override
    public String select() {
        return "MyBatis";
    }
}

去测试类中进行测试

package com.nowcoder.community;

import com.nowcoder.community.dao.AlphaDao;
import org.junit.jupiter.api.Test;
import org.springframework.beans.BeansException;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.test.context.ContextConfiguration;

@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
class CommunityApplicationTests implements ApplicationContextAware {

    private ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    @Test
    public void TestApplicationContext() {
        System.out.println(applicationContext);

        //按照接口类型返回bean,优先返回加了 @Primary 的bean
        AlphaDao alphaDao = applicationContext.getBean(AlphaDao.class);
        System.out.println(alphaDao.select());

        //按照bean的名字,返回指定的bean
        AlphaDao alphaDao2 = (AlphaDao) applicationContext.getBean("alphaHibernate");
        System.out.println(alphaDao2.select());
    }
}

bean的初始化与销毁

package com.nowcoder.community.service;

import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

@Service
@Scope("prototype") //默认是 singleton 单例模式
public class AlphaService {

    public AlphaService() {
        System.out.println("实例化AlphaService");
    }

    @PostConstruct //表示这个方法会在构造器之后调用
    public void init() {
        System.out.println("初始化AlphaService");
    }

    @PreDestroy //表示这个方法在销毁对象之前调用
    public void destory() {
        System.out.println("销毁AlphaService");
    }
}

测试类中新增测试方法

Spring 默认是单例模式,如果不加 @Scope("prototype"),哪怕多次 getBean() ,也都是获取的同一个对象,AlphaService 只会被实例化一次、初始化一次、销毁一次。加了该注解之后,每次 getBean() 都会被实例化一次

@Test
public void testBeanManagement() {
    AlphaService alphaService = applicationContext.getBean(AlphaService.class);
    System.out.println(alphaService);

    AlphaService alphaService2 = applicationContext.getBean(AlphaService.class);
    System.out.println(alphaService2);
}

获取第三方bean

前面的类都是我们自己写的,而有些类是第三方的,我们只能使用不能修改,如何将这种类装配成bean ?

写个配置类

package com.nowcoder.community.Config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.text.SimpleDateFormat;

@Configuration //表示这是一个配置类
public class AlphaConfig {

    @Bean //方法名就是 bean 的名字,该方法返回的对象将会被装配到Spring中
    public SimpleDateFormat simpleDateFormat() {
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    }
}

新增测试方法

@Test
public void testBeanConfig() {
    SimpleDateFormat simpleDateFormat =
            applicationContext.getBean(SimpleDateFormat.class);
    System.out.println(simpleDateFormat.format(new Date()));
}

我们想要获取 SimpleDateFormat 对象,使用 getBean() 还是太笨重了,可以直接使用 @Resource
依赖注入的方式

@Resource
//该接口下对应多个实现类,使用这个注解指定想要获取的bean的名字
@Qualifier("alphaHibernate") 
private AlphaDao alphaDao;

@Test
public void testDI() {
    System.out.println(alphaDao);
}

3. SpringMVC入门

Thymeleaf

数据都封装在 Model 对象中,这里的模板文件指的就是 html 文件
在这里插入图片描述
在 application.properties 中关掉 thymeleaf 缓存(不关缓存的话,修改页面之后可能不生效)

spring.thymeleaf.cache=false

AlphaController 类中增加 controller

@RequestMapping("/http")
public void http(HttpServletRequest request, HttpServletResponse response) {
    //获取请求数据
    System.out.println(request.getMethod());//获取请求方式
    System.out.println(request.getServletPath());//获取请求路径
    Enumeration<String> headerNames = request.getHeaderNames();
    while (headerNames.hasMoreElements()) {
        String name = headerNames.nextElement();
        String value = request.getHeader(name);
        System.out.println(name + ": " + value);
    }
    System.out.println(request.getParameter("code"));

    //返回相应数据
    response.setContentType("text/html;charset=utf-8");
    try (PrintWriter writer = response.getWriter()) {//放在()里会自动关闭该流
        writer.write("<h1>牛客网<h1>");
    } catch (IOException e) {
        e.printStackTrace();
    }
}

启动主程序,浏览器访问 http://localhost:8080/community/alpha/http,数据成功回显!

3.1 请求数据

GET请求

两种访问方式

AlphaController 类

第一种访问方式

//GET请求,常用于获取服务器的数据,是浏览器的默认请求方式
//路径:/student?current=2&limit=10
@RequestMapping(path = "/student", method = RequestMethod.GET)
@ResponseBody
public String getStudents(
        //这个注解的意思是,将浏览器端current的值传给这个形参current,false表示这个参数可以不传
        @RequestParam(name = "current", required = false, defaultValue = "1") int current,
        int limit) {//在浏览器端传的参数,名称一致时,会自动将值匹配给相应的形参

    System.out.println(current);
    System.out.println(limit);
    return "some students";
}

访问 http://localhost:8080/community/alpha/student?current=2&limit=10,current也可以不写

这种访问方式是将参数通过 ? 拼接在路径之后,下面使用另一种访问方式:参数作为访问路径的一部分

第二种访问方式

//路径:/student/123
@RequestMapping(path = "/student/{id}", method = RequestMethod.GET)
@ResponseBody
public String getStudent(@PathVariable("id") int id) {//这个注解会将路径中的参数id赋值给形参id
    System.out.println(id);
    return "a students";
}

访问 http://localhost:8080/community/alpha/student/111

POST请求
在 resources/static 下新建目录 html,新建 student.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>增加学生</title>
</head>
<body>
    <form method="post" action="/community/alpha/addStudent">
        <p>
            姓名:<input type="text" name="name">
        </p>
        <p>
            年龄:<input type="text" name="age">
        </p>
        <p>
            <input type="submit" value="提交">
        </p>
    </form>
</body>
</html>

AlphaController 类中增加访问路径

@PostMapping("/addStudent")
@ResponseBody
public String addStudent(String name, int age) {
    System.out.println(name);
    System.out.println(age);
    return "success";
}

表单里的 “name” 和 “age” 和形参名一致才能传过去

访问 http://localhost:8080/community/html/student.html,填入数据并提交,数据成功传到后台!

3.2 响应数据

响应视图

AlphaController 类新增 controller

model存数据,view是视图 (html文件),交给 thymeleaf 拼接

//响应html数据
@GetMapping("/teacher")
public ModelAndView getTeacher() {
    ModelAndView mv = new ModelAndView();
    mv.addObject("name", "张三");
    mv.addObject("age", "30");
    //指定视图名
    mv.setViewName("/demo/view");// templates/demo/view.html
    return mv;
}

resources/templates 下新建目录 demo,新建 view.html

<!DOCTYPE html >
<html lang="en" xmlns:th="http://www.thymeleaf.org"> <!--表明这是个模板文件,交由thymeleaf拼接-->
<head>
    <meta charset="UTF-8">
    <title>Teacher</title>
</head>
<body>
    <p th:text="${name}"></p>
    <p th:text="${age}"></p>
</body>
</html>

访问 http://localhost:8080/community/alpha/teacher,查到数据!

第二种方式:

@GetMapping("/teacher2")
public String getTeacher(Model model) { //String:视图名
    model.addAttribute("name", "李四");
    model.addAttribute("age", "50");
    return "demo/view";
}

响应JSON字符串

@GetMapping("/emp")
@ResponseBody //这个注解表示返回的是JSON字符串
public Map<String, Object> getEmp() {
    Map<String, Object> emp = new HashMap<>();
    emp.put("name", "张三");
    emp.put("age", "23");
    emp.put("salary", "8000");
    return emp;
}

访问 http://localhost:8080/community/alpha/emp,效果如下:

{"name":"张三","salary":"8000","age":"23"}

如果返回的是多个员工的集合呢?

@GetMapping("/emps")
@ResponseBody
public List<Map<String, Object>> getEmps() {
    List<Map<String, Object>> emps = new ArrayList<>();
    Map<String, Object> emp = new HashMap<>();
    emp.put("name", "张三");
    emp.put("age", "23");
    emp.put("salary", "8000");
    emps.add(emp);

    emp = new HashMap<>();
    emp.put("name", "李四");
    emp.put("age", "33");
    emp.put("salary", "9000");
    emps.add(emp);

    emp = new HashMap<>();
    emp.put("name", "王五");
    emp.put("age", "43");
    emp.put("salary", "12000");
    emps.add(emp);

    return emps;
}

访问 http://localhost:8080/community/alpha/emps,效果如下:

[{"name":"张三","salary":"8000","age":"23"},{"name":"李四","salary":"9000","age":"33"},{"name":"王五","salary":"12000","age":"43"}]

4. MyBatis入门

本项目使用 MySQL 8

在这里插入图片描述


执行 init_schema.sql 文件,创建相关的表

执行 init_data.sql 文件,向表中插入相关数据

MyBatis

核心组件

  • SqlSessionFactory:用于创建SqlSession的工厂类。
  • SqlSession:MyBatis的核心组件,用于向数据库执行SQL。
  • 主配置文件:XML配置文件,可以对MyBatis的底层行为做出详细的配置。
  • Mapper接口:就是DAO接口,在MyBatis中习惯性的称之为Mapper。
  • Mapper映射器:用于编写SQL,并将SQL和实体类映射的组件,采用XML、注解均可实现。

pom.xml 添加依赖

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.16</version>
</dependency>

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.1</version>
</dependency>

修改配置文件 application.properties,添加如下配置

# DataSourceProperties
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/community?characterEncoding=utf-8&useSSL=false&serverTimezone=Hongkong
spring.datasource.username=root
spring.datasource.password=password
spring.datasource.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.hikari.maximum-pool-size=15
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.idle-timeout=30000

# MybatisProperties
mybatis.mapper-locations=classpath:mapper/*.xml
mybatis.type-aliases-package=com.nowcoder.community.entity
mybatis.configuration.useGeneratedKeys=true
mybatis.configuration.mapUnderscoreToCamelCase=true

新建实体包 entity,新建实体类 User

package com.nowcoder.community.entity;

import java.util.Date;

public class User {
    private int id;
    private String username;
    private String password;
    private String salt;
    private String email;
    private int type;
    private int status;
    private String activationCode;
    private String headerUrl;
    private Date createTime;

    public int getId() {
        return id;
    }

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

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getSalt() {
        return salt;
    }

    public void setSalt(String salt) {
        this.salt = salt;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public int getType() {
        return type;
    }

    public void setType(int type) {
        this.type = type;
    }

    public int getStatus() {
        return status;
    }

    public void setStatus(int status) {
        this.status = status;
    }

    public String getActivationCode() {
        return activationCode;
    }

    public void setActivationCode(String activationCode) {
        this.activationCode = activationCode;
    }

    public String getHeaderUrl() {
        return headerUrl;
    }

    public void setHeaderUrl(String headerUrl) {
        this.headerUrl = headerUrl;
    }

    public Date getCreateTime() {
        return createTime;
    }

    public void setCreateTime(Date createTime) {
        this.createTime = createTime;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                ", salt='" + salt + '\'' +
                ", email='" + email + '\'' +
                ", type=" + type +
                ", status=" + status +
                ", activationCode='" + activationCode + '\'' +
                ", headerUrl='" + headerUrl + '\'' +
                ", createTime=" + createTime +
                '}';
    }
}

dao 目录下新建接口 UserMapper

package com.nowcoder.community.dao;

import com.nowcoder.community.entity.User;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface UserMapper {

    User selectById(int id);

    User selectByName(String username);

    User selectByEmail(String email);

    int insertUser(User user);

    int updateStatus(int id, int status);

    int updateHeader(int id, String headerUrl);

    int updatePassword(int id, String password);
}

resources目录下新建目录 mapper,新建 user-mapper.xml

<?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.nowcoder.community.dao.UserMapper">
    
    <sql id="selectFields">
        id, username, password, salt, email, type, status, activation_code, header_url, create_time
    </sql>

    <sql id="insertFields">
        username, password, salt, email, type, status, activation_code, header_url, create_time
    </sql>

	<!--#{id},表示引用方法 selectById(int id) 的参数-->
    <select id="selectById" resultType="User">
        select <include refid="selectFields"></include>
        from user
        where id = #{id}
    </select>

    <select id="selectByName" resultType="User">
        select <include refid="selectFields"></include>
        from user
        where username = #{username}
    </select>

    <select id="selectByEmail" resultType="User">
        select <include refid="selectFields"></include>
        from user
        where email = #{email}
    </select>
    
    <insert id="insertUser" parameterType="User" keyProperty="id">
        insert into user (<include refid="insertFields"></include>)
        values (#{username}, #{password}, #{salt}, #{email}, #{type}, #{status}, #{activationCode}, #{headerUrl}, #{createTime})
    </insert>

    <update id="updateStatus">
        update user set status = #{status} where id = #{id}
    </update>

    <update id="updateHeader">
        update user set header_url = #{headerUrl} where id = #{id}
    </update>

    <update id="updatePassword">
        update user set password = #{password} where id = #{id}
    </update>
</mapper>

插入用户会引起主键自增,keyProperty="id" 可以保证 MyBatis 获取数据库中的主键封装到新插入的 User 中

新建测试类

package com.nowcoder.community;

import com.nowcoder.community.dao.UserMapper;
import com.nowcoder.community.entity.User;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;

@SpringBootTest
public class MapperTests {

    @Resource
    private UserMapper userMapper;

    @Test
    public void testSelectUser() {
        User user = userMapper.selectById(101);
        System.out.println(user);

        user = userMapper.selectByName("liubei");
        System.out.println(user);

        user = userMapper.selectByEmail("nowcoder101@sina.com");
        System.out.println(user);
    }
}

运行,成功查到三个用户!

测试 insert

@Test
public void testInsertUser() {
    User user = new User();
    user.setUsername("test");
    user.setPassword("123456");
    user.setSalt("abc");
    user.setEmail("test@qq.com");
    user.setHeaderUrl("http://nowcoder.com/101.png");
    user.setCreateTime(new Date());

    int rows = userMapper.insertUser(user);
    System.out.println(rows);
    System.out.println(user.getId());
}

测试 update

【踩坑】
遇到异常:
Parameter ‘status’ not found. Available parameters are [arg1, arg0, param1, param2]

【解决】
将 UserMapper 类的三个 update 方法添加 @Param 注解(有多个形参时需要添加),起个别名

int updateStatus(@Param("id") int id, @Param("status") int status);

int updateHeader(@Param("id")int id, @Param("headerUrl") String headerUrl);

int updatePassword(@Param("id")int id, @Param("password") String password);

为了方便排错,修改 MyBatis 的日志级别
application.properties 增加如下配置

# logger
logging.level.com.nowcoder.community=debug

5. 社区首页

5.1 dao

帖子表是 discuss_post,在 entity 目录下新建对应的实体类 DiscussPost

package com.nowcoder.community.entity;

import java.util.Date;

public class DiscussPost {
    private int id;
    private int userId;
    private String title;
    private String content;
    private int type;
    private int status;
    private Date createTime;
    private int commentCount;
    private double score;

    public int getId() {
        return id;
    }

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

    public int getUserId() {
        return userId;
    }

    public void setUserId(int userId) {
        this.userId = userId;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public int getType() {
        return type;
    }

    public void setType(int type) {
        this.type = type;
    }

    public int getStatus() {
        return status;
    }

    public void setStatus(int status) {
        this.status = status;
    }

    public Date getCreateTime() {
        return createTime;
    }

    public void setCreateTime(Date createTime) {
        this.createTime = createTime;
    }

    public int getCommentCount() {
        return commentCount;
    }

    public void setCommentCount(int commentCount) {
        this.commentCount = commentCount;
    }

    public double getScore() {
        return score;
    }

    public void setScore(double score) {
        this.score = score;
    }

    @Override
    public String toString() {
        return "DiscussPost{" +
                "id=" + id +
                ", userId=" + userId +
                ", title='" + title + '\'' +
                ", content='" + content + '\'' +
                ", type=" + type +
                ", status=" + status +
                ", createTime=" + createTime +
                ", commentCount=" + commentCount +
                ", score=" + score +
                '}';
    }
}

dao 层的 mapper

package com.nowcoder.community.dao;

import com.nowcoder.community.entity.DiscussPost;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

@Mapper
public interface DiscussPostMapper {

    /**
     *
     * @param userId 若为0,则表示查询所有用户的所有帖子
     * @param offset 每一页起始行的行号
     * @param limit  每一页显示多少个
     * @return
     */
    List<DiscussPost> selectDiscussPosts(
            @Param("userId") int userId,
            @Param("offset") int offset,
            @Param("limit") int limit);

    //动态拼接sql,并且只有一个参数,必须使用@Param
    int selectDiscussPostRows(@Param("userId") int userId);
}

映射文件,resources/mapper 下新建 discusspost-mapper.xml

<?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.nowcoder.community.dao.DiscussPostMapper">

    <sql id="selectFields">
        id, user_id, title, content, type, status, create_time, comment_count, score
    </sql>
    
    <select id="selectDiscussPosts" resultType="DiscussPost">
        select <include refid="selectFields"></include>
        from discuss_post
        where status != 2
        <if test="userId != 0">
            and user_id = #{userId}
        </if>
        order by type desc, create_time desc
        limit #{offset}, #{limit}
    </select>
    
    <select id="selectDiscussPostRows" resultType="int">
        select count(id)
        from discuss_post
        where status != 2
        <if test="userId != 0">
            and user_id = #{userId}
        </if>
    </select>
</mapper>

limit #{offset}, #{limit}
表示从 offset 行开始显示 #{limit} 个数据

测试类 MapperTests 中写个测试方法

@Resource
private DiscussPostMapper discussPostMapper;

@Test
public void testSelectPosts() {
    List<DiscussPost> list = discussPostMapper.selectDiscussPosts(0, 0, 10);
    for (DiscussPost post : list) {
        System.out.println(post);
    }
    
    int rows = discussPostMapper.selectDiscussPostRows(0);
    System.out.println(rows);
}

效果:
在这里插入图片描述

5.2 service

帖子相关的service

package com.nowcoder.community.service;

import com.nowcoder.community.dao.DiscussPostMapper;
import com.nowcoder.community.entity.DiscussPost;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.List;

@Service
public class DiscussPostService {

    @Resource
    private DiscussPostMapper discussPostMapper;

    public List<DiscussPost> findDiscussPosts(int userId, int offset, int limit) {
        return discussPostMapper.selectDiscussPosts(userId, offset, limit);
    }

    public int findDiscussPostRows(int userId) {
        return discussPostMapper.selectDiscussPostRows(userId);
    }
}

user相关的service

package com.nowcoder.community.service;

import com.nowcoder.community.dao.UserMapper;
import com.nowcoder.community.entity.User;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service
public class UserService {

    @Resource
    private UserMapper userMapper;

    public User findUserById(int id) {
        return userMapper.selectById(id);
    }
}

将相关前端资源复制到项目下

在这里插入图片描述

5.3 controller

package com.nowcoder.community.controller;

import com.nowcoder.community.entity.DiscussPost;
import com.nowcoder.community.entity.User;
import com.nowcoder.community.service.DiscussPostService;
import com.nowcoder.community.service.UserService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Controller
public class HomeController {

    @Resource
    private DiscussPostService discussPostService;

    @Resource
    private UserService userService;

    @GetMapping("/index")
    public String getIndexPage(Model model) {
        //这个list里的帖子含有外键userId,我们需要查到userName拼接到帖子上
        List<DiscussPost> list = discussPostService.findDiscussPosts(0, 0, 10);
        List<Map<String, Object>> discussPosts = new ArrayList<>();
        if (list != null) {
            for (DiscussPost post : list) {
                Map<String, Object> map = new HashMap<>();
                map.put("post", post);
                User user = userService.findUserById(post.getUserId());
                map.put("user", user);
                discussPosts.add(map);
            }
        }
        model.addAttribute("discussPosts", discussPosts);
        return "/index";
    }

}

修改 index.html
第二行加入命名空间

<html lang="en" xmlns:th="http://www.thymeleaf.org">

第八行,thymeleaf接管 css

<link rel="stylesheet" th:href="@{/css/global.css}" />

最后面两个 script 标签也是

<script th:src="@{/js/global.js}"></script>
<script th:src="@{/js/index.js}"></script>

删掉 “帖子列表” ul 标签内的其他 li 标签,只留一个 li 标签即可

<!-- 帖子列表 -->
<ul class="list-unstyled">
	<li class="media pb-3 pt-3 mb-3 border-bottom" th:each="map:${discussPosts}">
		<a href="site/profile.html">
			<img th:src="${map.user.headerUrl}" class="mr-4 rounded-circle" alt="用户头像" style="width:50px;height:50px;">
		</a>
		<div class="media-body">
			<h6 class="mt-0 mb-3">
				<a href="#" th:utext="${map.post.title}">备战春招,面试刷题跟他复习,一个月全搞定!</a>
				<span class="badge badge-secondary bg-primary" th:if="${map.post.type==1}">置顶</span>
				<span class="badge badge-secondary bg-danger" th:if="${map.post.status==1}">精华</span>
			</h6>
			<div class="text-muted font-size-12">
				<u class="mr-3" th:text="${map.user.username}">寒江雪</u> 发布于 <b th:text="${#dates.format(map.post.createTime, 'yyyy-MM-dd HH:mm:ss')}">2019-04-15 15:32:18</b>
				<ul class="d-inline float-right">
					<li class="d-inline ml-2">赞 11</li>
					<li class="d-inline ml-2">|</li>
					<li class="d-inline ml-2">回帖 7</li>
				</ul>
			</div>
		</div>						
	</li>
</ul>

【踩坑】
如果表达式爆红了,比如 "${map.user.headerUrl}" ,是因为 thymeleaf 校验问题,解决如下:

在这里插入图片描述

访问 http://localhost:8080/community/index,成功!

5.4 完善分页功能

也就是页面底部的这一块:一共5页并不是写死的,而是由数据库中查询到的,每页显示几条也是不是写死的,我们只显示当前页左右相邻两页的范围,并把当前页的页码高亮显示
在这里插入图片描述

在 entity 目录下新建分页类

package com.nowcoder.community.entity;

/**
 * 封装分页信息
 */
public class Page {

    //当前页码
    private int current = 1;
    //显示上限
    private int limit = 10;
    //数据总数(用于计算总的页数)
    private int rows;
    //查询路径(用于复用分页链接)
    private String path;

    public int getCurrent() {
        return current;
    }

    public void setCurrent(int current) {
        if (current >= 1) {
            this.current = current;
        }
    }

    public int getLimit() {
        return limit;
    }

    public void setLimit(int limit) {
        if (limit >= 1 && limit <= 100) {
            this.limit = limit;
        }
    }

    public int getRows() {
        return rows;
    }

    public void setRows(int rows) {
        if (rows >= 0) {
            this.rows = rows;
        }
    }

    public String getPath() {
        return path;
    }

    public void setPath(String path) {
        this.path = path;
    }

    /**
     * 获取当前页的起始行
     * @return
     */
    public int getOffset() {
        return (current - 1) * limit;
    }

    //获取总页数
    public int getTotal() {
        if (rows % limit == 0) {
            return rows / limit;
        } else {
            return rows / limit + 1;
        }
    }

    //获取起始页码
    public int getFrom() {
        int from= current - 2;
        return from < 1 ? 1 : from;
    }

    //获取结束页码
    public int getTo() {
        int to= current + 2;
        int total = getTotal();
        return to > total ? total : to;
    }
}

改造 HomeController

@GetMapping("/index")
public String getIndexPage(Model model, Page page) {
    //方法调用前, SpringMVC会自动实例化Model和Page,并将Page注入Model.
    // 所以,在thymeleaf中可以直接访问Page对象中的数据.
    page.setRows(discussPostService.findDiscussPostRows(0));
    page.setPath("/index");

    //这个list里的帖子含有外键userId,我们需要查到userName拼接到帖子上
    List<DiscussPost> list = discussPostService.findDiscussPosts(0, page.getOffset(), page.getLimit());
    List<Map<String, Object>> discussPosts = new ArrayList<>();
    if (list != null) {
        for (DiscussPost post : list) {
            Map<String, Object> map = new HashMap<>();
            map.put("post", post);
            User user = userService.findUserById(post.getUserId());
            map.put("user", user);
            discussPosts.add(map);
        }
    }
    model.addAttribute("discussPosts", discussPosts);
    return "/index";
}

修改 index.html 的分页标签

<!-- 分页 -->
<nav class="mt-5" th:if="${page.rows > 0}">
	<ul class="pagination justify-content-center">
		<li class="page-item">
			<!--这个()就是替代的 ?,用于路径后面拼接变量 -->
			<a class="page-link" th:href="@{${page.path}(current = 1)}">首页</a>
		</li>
		<!--如果是第一页,则"上一页"不可点,通过 disabled 实现-->
		<li th:class="|page-item ${page.current == 1 ? 'disabled' : ''}|">
			<a class="page-link" th:href="@{${page.path}(current = ${page.current - 1})}">上一页</a>
		</li>
		<!--将当前页高亮显示页码,通过 active 实现-->
		<li th:class="|page-item ${page.current == i ? 'active' : ''}| " th:each="i:${#numbers.sequence(page.from, page.to)}">
			<a class="page-link" th:href="@{${page.path}(current=${i})}" th:text="${i}">1</a>
		</li>
		<!--如果是最后一页,则"下一页"不可点,通过 disabled 实现-->
		<li th:class="|page-item ${page.current == page.total ? 'disabled' : ''}|">
			<a class="page-link" th:href="@{${page.path}(current = ${page.current + 1})}">下一页</a>
		</li>
		<li class="page-item">
			<a class="page-link" th:href="@{${page.path}(current = ${page.total})}">末页</a>
		</li>
	</ul>
</nav>

对于上述代码中出现的 page.from, page.to 等,本质上程序会去调用 page 相应的 get 方法来取值,所以,即便在 discusspost-mapper.xml 中

<select id="selectDiscussPosts" resultType="DiscussPost">
...
limit #{offset}, #{limit}
</select>

page 对象并没有 offset 属性依然没有关系,因为 page 有 getOffset() 方法!


重新编译项目,访问页面,首页、末页,上一页、下一页跳转正常,仅显示相邻的两页!在第一页时,上一页点了无效,最后一页时,点击下一页也是无效!

在这里插入图片描述

6. 项目调试

  • 响应状态码的含义
  • 服务端断点调试技巧
  • 客户端断点调试技巧
  • 设置日志级别,并将日志输出到不同的终端

服务端断点调试技巧

在这里插入图片描述

访问 http://localhost:8080/community/index,程序会卡在断点处,

快捷键功能
F7在 Debug 模式下,进入下一步,如果当前行断点是一个方法,则进入当前方法体内,如果该方法体还有方法,则不会进入该内嵌的方法中
F8在 Debug 模式下,进入下一步,如果当前行断点是一个方法,则不进入当前方法体内
F9在 Debug 模式下,恢复程序运行,但是如果该断点下面代码还有断点则停在下一个断点上

断点管理界面:

在这里插入图片描述

取消某一断点的对钩,该断点将变为空心圆,表示不可用,想要删除断点,选中若干断点,点击上面的 “-” 即可。

客户端断点调试技巧

就是调试 js 代码,在浏览器中打断点

设置日志级别

application.properties

# logger
logging.level.com.nowcoder.community=debug

新建测试类

package com.nowcoder.community;

import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class LoggerTests {

    private static final Logger logger = LoggerFactory.getLogger(LoggerTests.class);

    @Test
    public void testLogger() {
        System.out.println(logger.getName());
        logger.debug("debug log");
        logger.info("info log");
        logger.warn("warn log");
        logger.error("error log");
    }
}

运行结果:

com.nowcoder.community.LoggerTests
2022-06-14 18:49:33.661 DEBUG 9276 --- [           main] com.nowcoder.community.LoggerTests       : debug log
2022-06-14 18:49:33.661  INFO 9276 --- [           main] com.nowcoder.community.LoggerTests       : info log
2022-06-14 18:49:33.661  WARN 9276 --- [           main] com.nowcoder.community.LoggerTests       : warn log
2022-06-14 18:49:33.661 ERROR 9276 --- [           main] com.nowcoder.community.LoggerTests       : error log

修改日志输出位置,输出到文件中
application.properties

logging.file.name=d:/work/data/nowcoder/community.log

重新运行测试方法,生成了日志文件

在这里插入图片描述

工作中,一般将不同级别的日志输出到不同文件中,且文件达到一定大小时需要换个文件

resources目录下新建 logback-spring.xml (名字必须是这个)

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <contextName>community</contextName>
    <property name="LOG_PATH" value="d:/work/data"/> <!--不能写D:-->
    <property name="APPDIR" value="community"/>

    <!-- error file -->
    <appender name="FILE_ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_PATH}/${APPDIR}/log_error.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_PATH}/${APPDIR}/error/log-error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>5MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <append>true</append>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>%d %level [%thread] %logger{10} [%file:%line] %msg%n</pattern>
            <charset>utf-8</charset>
        </encoder>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>error</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <!-- warn file -->
    <appender name="FILE_WARN" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_PATH}/${APPDIR}/log_warn.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_PATH}/${APPDIR}/warn/log-warn-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>5MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <append>true</append>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>%d %level [%thread] %logger{10} [%file:%line] %msg%n</pattern>
            <charset>utf-8</charset>
        </encoder>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>warn</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <!-- info file -->
    <appender name="FILE_INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_PATH}/${APPDIR}/log_info.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_PATH}/${APPDIR}/info/log-info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>5MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <append>true</append>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>%d %level [%thread] %logger{10} [%file:%line] %msg%n</pattern>
            <charset>utf-8</charset>
        </encoder>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>info</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <!-- console -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d %level [%thread] %logger{10} [%file:%line] %msg%n</pattern>
            <charset>utf-8</charset>
        </encoder>
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>debug</level>
        </filter>
    </appender>

    <logger name="com.nowcoder.community" level="debug"/>

    <root level="info">
        <appender-ref ref="FILE_ERROR"/>
        <appender-ref ref="FILE_WARN"/>
        <appender-ref ref="FILE_INFO"/>
        <appender-ref ref="STDOUT"/>
    </root>

</configuration>

运行测试方法,效果如下:

在这里插入图片描述


评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值