Bootstrap

(仿牛客论坛项目)01 - 开发社区首页


前言

第一个大的项目,具体其中的技术细节就不做笔记了,需要的可以自己去牛客官网学习;不过其中没有具体讲这个技术细节,只是教会你该怎么用;
视频链接:https://www.nowcoder.com/study/live/246


1、做项目的步骤

  1. 需求;
  2. 抽取实体,建数据库,建表,给表中添加一些测试数据;
  3. 根据数据库的表建立 pojo/entity 类;(下面所有的 pojo 类都忽略 get/set 方法
  4. 分步实现功能:DAO -> Service 层 -> Controller 层 -> html 静态页面

注意:

  • 所有的数据库表和静态页面可以从官网获取。

2、开发社区首页功能分步

在这里插入图片描述
一些前期的准备工作,顺便测试一下数据库连接:

2.1 User 类

  1. 根据数据库表建立的 pojo 类,采用驼峰映射法,配置文件中已经开启了驼峰映射,mybatis.configuration.mapUnderscoreToCamelCase=true
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;

    @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 +
                '}';
    }
}

2.2 UserMapper 接口

  1. 定义一些 DAO 层要用到的和数据库交互的增删改查方法;
  2. 上面加上 @Mapper 注解将当前类标识为一个 mapper 接口;
@Mapper
public interface UserMapper {
    User selectById(int id);

    //根据用户名来查询,用户名是唯一的
    User selectByName(String username);

    User selectByEmail(String email);

    int insertUser(User user);

    //以id为条件更新一些参数:状态、头像路径、密码
    int updateStatus(int id, int status);

    int updateHeader(int id, String headerUrl);

    int updatePassword(int id, String password);
}

2.3 UserMapper 映射文件

  1. 具体的实现 DAO 层功能的文件,namespace 中接口的全类名不能写错;
<?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="insertFields">
        username, password, salt, email, type, status, activation_code, header_url, create_time
    </sql>

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

    <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>

2.4 编写测试类

  1. 编写测试类,测试和数据库连接是否有问题,能否正确从数据库获取数据;
@SpringBootTest
public class MapperTests {
    @Autowired
    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("[email protected]");
        System.out.println(user);
    }

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

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

    @Test
    public void updateUser() {
        int rows = userMapper.updateStatus(150, 1);
        System.out.println(rows);

        rows = userMapper.updateHeader(150, "http://www.nowcoder.com/102.png");
        System.out.println(rows);

        rows = userMapper.updatePassword(150, "hello");
        System.out.println(rows);
    }
}

3、开发社区首页,显示前10个帖子

3.1 DiscussPost 类

  1. 完全对应表 discuss_post 来建立;
  2. 这里的 userId 不要写成 User 实体类,后面在 Controller 类中进行关联即可,否则要在 Mapper 映射类中进行多对一关系映射,不利于后面实现 Redis 存储。
/**
 * 对应表discuss_post
 */
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;

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

3.2 DiscussPostMapper 接口

  1. 实现查询所有帖子功能;
  2. 考虑到之后要实现登录之后显示我的帖子功能,所以这里添加一个条件userId,并且使用动态 sql 来实现拼接;
  3. 考虑到之后可能要分页显示,所以形参中加上 offset-偏移量 和 limit-每页显示多少帖子 数据;
@Mapper
public interface DiscussPostMapper {
    //查询所有帖子,要实现登录之后显示我的帖子,也要实现分页显示的功能
    List<DiscussPost> selectAllDiscussPosts(int userId,int offset,int limit);

    //查询一共有多少条数据
    // @Param注解用于给参数取别名,如果只有一个参数,并且在<if>里使用,则必须加别名.
    int selectDiscussPostRows(@Param("userId") int userId);
}

注意:

  • @Param 注解用于给参数取别名,如果只有一个参数,并且在 sql 动态拼接中即 <if> 标签里使用,则必须加别名。

3.3 DiscussPostMapper 映射文件

  1. 尽量在 sql 语句中不要使用 * 来显示查询数据,要使用字段,sql 优化之一;
  2. 在映射文件中将所有的增查的 sql 字段提取出来作为一个公用的 sql 语句,这样之后想修改查询的字段的时候,一处修改,处处生效;
  3. 要注意标签内部的语句都是 sql 字段,标签中对应的都是 java 类,包括 #{java属性} 中使用的也是 java 某实体类中对应的属性名称。
<?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>
    <!--List<DiscussPost> selectAllDiscussPosts(int userId,int offset,int limit);-->
    <select id="selectAllDiscussPosts" 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>

    <!--int selectDiscussPostRows(@Param("userId") int userId);-->
    <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>

解释:

  • 如果你从一开始

3.4 编写测试类

@SpringBootTest
public class DiscussPostTest {
    @Autowired
    private DiscussPostMapper discussPostMapper;

    //用户输入为null表示首页展示
    @Test
    public void testSelectAllDiscussPosts(){
        List<DiscussPost> discussPosts = discussPostMapper.selectAllDiscussPosts(0, 0, 10);
        discussPosts.forEach(System.out::println);
    }

    @Test
    public void testSelectDiscussPostRows(){
        int discussPostRows = discussPostMapper.selectDiscussPostRows(0);
        System.out.println(discussPostRows);
    }
}

3.5 UserService 层

  1. 查询出所有的帖子之后,在主页要显示每个帖子对应的用户头像,所以还要将用户信息查询出来;
@Service
public class UserServiceImpl implements UserService {
    @Autowired
    private UserMapper userMapper;

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

3.6 HomeController 层

  1. 显示前 10 个帖子:先实现查询10条数据,查询所有帖子的 userId 对应的 User 数据, 将所有的 User 和 DiscussPost 中的 userId 关联起来;
  2. 将帖子放在作用域中,返回视图名称,这里返回视图名称是给 thymeleaf 模板解析的,所以会自动加上试图前缀和视图后缀;
@Controller
public class HomeController {
    @Autowired
    private DiscussPostService discussPostService;

    @Autowired
    private UserService userService;

    //实现主页功能
    @RequestMapping(value = "/index", method = RequestMethod.GET)
    public String index(Model model) {
        List<DiscussPost> list = discussPostService.findDiscussPosts(0, 0, 10);
        //使用一个map容器将所有的User和DiscussPost中的userId关联起来
        List<Map<String, Object>> discussPosts = new ArrayList<>();
        if (list != null) {
            for (DiscussPost discussPost : list) {
                HashMap<String, Object> map = new HashMap<>();
                map.put("discussPost", discussPost);
                //根据userId获取User对象
                User user = userService.findUserById(discussPost.getUserId());
                map.put("user", user);
                //将这一对关联的对象放进list集合中
                discussPosts.add(map);
            }
        }
        model.addAttribute("discussPosts", discussPosts);
        return "index";
    }
}

3.7 修改 index.html 文件

  1. 将文件引用路径改成 thymeleaf 格式的;

    1. 在 html 表头标签加上:xmlns:th="http://www.thymeleaf.org"

    2. 引入外部 css 样式加上:th:href="@{/css/global.css}"

  2. 处理帖子列表部分的内容:

    • 遍历所有的帖子:th:each="map:${discussPosts}"
    • 显示用户头像:th:src="${map.user.headerUrl}"
    • 显示帖子title:有可能会包含转义字符的情况下使用 th:utext 标签,th:utext="${map.discussPost.title}"
    • 判断是否是置顶类型,也就是type==1,th:if="${map.discussPost.type==1}"
    • 判断是否是精华帖子,th:if="${map.discussPost.status==1}"
    • 修改帖子的作者:th:utext="${map.user.username}"
    • 修改帖子发布时间:th:text="${map.discussPost.createTime}"

注意:

  • 修改时间显示格式:th:text="${#dates.format(xxx.xxx.xxx,'yyyy-MM-dd HH:mm:ss')}"这种方法只能修改 Date 格式的日期,不能修改 LocalDateTime 格式(新日期)的数据);
<!-- 帖子列表 -->
<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.discussPost.title}">备战春招,面试刷题跟他复习,一个月全搞定!</a>
				<span class="badge badge-secondary bg-primary" th:if="${map.discussPost.type==1}">置顶</span>
				<span class="badge badge-secondary bg-danger" th:if="${map.discussPost.status==1}">精华</span>
			</h6>
			<div class="text-muted font-size-12">
				<u class="mr-3" th:utext="${map.user.username}">寒江雪</u> 发布于 <b th:text="${#dates.format(map.discussPost.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>

3.8 访问首页

  1. 访问 http://localhost:8080/community/index,观看是否可以显示正确的首页数据(10 条数据);

在这里插入图片描述

3.9 错误信息及问题解决

问题一:将所有的 User 和 DiscussPost 关联起来有两种方法

一种是在查询 sql 语句的时候关联查询用户,可以使用 association 处理一对多关系,也可以使用分步查询;

  1. 如果在开始新建 pojo 类的时候就将 userId 定义成 User 实体类,那么这里涉及到,JavaBean实体类怎么处理外键
  2. 对于一个实体类的属性是另一个实体类的时候,在编写 mapper 映射文件的时候需要涉及到实体类属性的参数设置,Mybatis mapper层 同时传入实体类和单独的参数

另外一种是在 HomeController 中单独的针对每个 DiscussPost,根据 userId 获取用户数据,并将他们关联起来;使用第二种,后面使用 redis 的时候更方便

4、开发分页组件,分页显示所有帖子

4.1 Page 组件

  1. 这里使用的时候直接复制粘贴即可,其实已经有封装好的分页插件可以使用;
/**
 * 封装分页相关的信息.
 */
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() {
        // current * limit - limit
        return (current - 1) * limit;
    }

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

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

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

4.2 HomeController 层

  1. 实现分页功能:getIndexPage(Model model,Page page)方法,设置总页数,设置复用的路径,将查找的数据变成所有的数据,而不是固定的10条
  2. 方法调用前,SpringMVC会自动实例化 Model 和 Page,并且将 page 注入 Model 中(不需要像 discussPosts 一样需要我们手动放入),所以 thymeleaf 可以直接访问 Page 对象中的数据;
//实现主页功能
@RequestMapping(value = "/index", method = RequestMethod.GET)
public String index(Model model, Page page) {
    //插入分页功能
    //设置总共页数,初始页面,是不存在用户信息的
    page.setRows(discussPostService.findDiscussPostRows(0));
    //设置复用的路径,这个是给浏览器发送请求用的,所以要加上/
    page.setPath("/index");


    List<DiscussPost> list = discussPostService.findDiscussPosts(0, page.getOffset(), page.getLimit());
    //使用一个map容器将所有的User和DiscussPost中的userId关联起来
    List<Map<String, Object>> discussPosts = new ArrayList<>();
    if (list != null) {
        for (DiscussPost discussPost : list) {
            HashMap<String, Object> map = new HashMap<>();
            map.put("discussPost", discussPost);
            //根据userId获取User对象
            User user = userService.findUserById(discussPost.getUserId());
            map.put("user", user);
            //将这一对关联的对象放进list集合中
            discussPosts.add(map);
        }
    }
    model.addAttribute("discussPosts", discussPosts);
    return "index";
}

4.3 修改 index.html 页面

  1. 处理分页的逻辑:(之后所有页面的分页逻辑都可以复用
  2. 如果一个帖子都没有的话,不显示页码,有帖子才显示分页逻辑:th:if="${page.rows>0}"
  3. 修改首页和末页逻辑: th:href="@{${page.path}(current=1)}",这里传送的请求其实就是: /index?current=1
  4. 修改上一页下一页逻辑:th:href="@{${page.path}(current=${page.total})}"
  5. 遍历显示中间的页码,生成从 page.from 到 page.to 之间的连续的一个数组:th:each="i:${#numbers.sequence(page.from,page.to)}",跳转到对应页面:th:href="@{${page.path}(current=${i})}"
  6. 当前在首页时候上一页不可点,末页的时候下一页不可点:th:class="|page-item ${page.current==1?'disabled':''}|"
  7. 当前在第几页,这个页码上高亮,通过 class 中的 active 效果来实现:th:class="|page-item ${i==current?'active':''}|"
<!-- 分页 -->
<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>
		<li th:class="|page-item ${page.current==1?'disabled':''}|">
			<a class="page-link" th:href="@{${page.path}(current=${page.current-1})}">上一页</a></li>
		<li th:class="|page-item ${i==page.current?'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>
		<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>

4.4 访问首页

  1. 访问 http://localhost:8080/community/index,观看是否可以显示正确的显示所有分页数据;

在这里插入图片描述

在这里插入图片描述

5、错误排查

5.1 由于低版本 Spring 不支持高版本 JDK 报错

一开始用的是 5.3.4 版本,发现会报错:初学Spring遇到Unsupported class file major version 61 错误

5.2 @RunWith 注解找不到

找不到 @RunWith 注解,高版本取消了这个注解,直接使用@SpringBootTest 注解就可以了,@RunWith注解找不到,怎么办?

5.3 想访问一个 servlet 却变成了下载一个东西,竟然只是写错了 text

访问一个页面,发送请求,没有得到想象中的响应页面,原因是粗心错误:访问一个servlet却直接下载文件

6、常用注解和命令

6.1 SpringBoot 常用注解

  1. @SpringBootApplication:定义主程序
  2. @SpringBootConfiguration:当前类当作一个配置文件
  3. @EnableAutoConfiguration:允许配置文件的自动装配
  4. @ComponentScan:自动扫描配置类所在的包以及所有子包下的bean
  5. 四个注解可以将一个类当成一个bean被扫描:@Component,@Controller,@Service,@Repository
  6. 在测试类中测试使用IOC容器,需要使用注解@ContextConfiguration(classes = CommunityApplication.class),这个测试类要implements ApplicationContextAware,重写其中的setApplicationContext方法
  7. @Primary:当使用 @Autowired 来根据类型进行属性注入的时候,或者 IOC 容器根据类型来进行 getBean() 装配的时候,优先被提取的类
  8. @Repository("helloDao"):字符串中定义bean的名字,默认是首字母小写的类名
  9. @PostConstruct:在类的构造器之后执行
  10. @PreDestroy:在类的销毁之前执行
  11. @Scope(value = "prototype"):定义多例bean

6.2 注意 MVC 框架三层和服务器三层组件的区别

在这里插入图片描述

  • 服务器三层组件:表现层、业务层、数据层;
  • SpringMVC 框架三层:Model、View、Controller,是表现层的一个框架;

6.3 request 常用的获取方法

  1. 获取请求方法、路径、请求头(先获取所有的键值对,然后根据键值遍历获取值)、请求参数
request.getMethod()
request.getServletPath()
request.getHeaderNames();request.getHeader(name);
request.getParameter("code")

6.4 数据库的命令行操作

  1. 数据库访问:mysql -uroot -p,输入这个之后回车输入密码是隐藏的;
  2. 修改密码:alter user root@localhost identified by 'abc123';,不要忘记最后的分号;
  3. 创建一个新的数据库:create database communtiy;
  4. 展示数据库列表:show databases;
  5. 删除数据库:drop database communtiy;
  6. 修改当前使用的数据库:use community;

6.5 MySQL 8.0 数据库中时间需要注意的点

  1. MySQL8.0 之后数据库 DataTime 类型的字段,对应 java 类发生了改变,由原来对应的 Date 类变成了变成了 LocalDateTime;
  2. timestamp 对应的还是 Data 类。
;