目录
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>
运行测试方法,效果如下: