课程介绍
课程目标
- 学会主流的 web 开发技术和框架
- 积累一个真实的 web 项目的开发经验
- 掌握热点面试题的答题策略
技术架构
- Spring Boot
- Spring、Spring MVC、MyBatis
- Redis、Kafka、Elasticsearch
- Spring Security、Spring Actuator
springboot 是简化 spring 的操作的。
开发环境
- 构建工具:Apache Maven 3.6.1
- 集成开发工具:IntelliJ IDEA
- 数据库:MySQL、Redis
- 应用服务器:Apache Tomcat
- 版本控制工具:Git
搭建开发环境
Apache Maven
-
可以帮助我们构建项目,管理项目中的 jar 包
-
Maven 仓库:存放构件的位置
- 本地仓库:默认是~/.m2/repository
- 远程仓库:中央仓库、镜像仓库、私服仓库
更改远程仓库
因为 maven 默认访问的是远程的私服仓库,非常慢,所以解压在官方下载好的压缩包后,conf 文件夹下的 setting.xml
改成阿里云的镜像仓库。
<mirrors>
<!-- mirror
| Specifies a repository mirror site to use instead of a given repository. The repository that
| this mirror serves has an ID that matches the mirrorOf element of this mirror. IDs are used
| for inheritance and direct lookup purposes, and must be unique across the set of mirrors.
|
<mirror>
<id>mirrorId</id>
<mirrorOf>repositoryId</mirrorOf>
<name>Human Readable Name for this Mirror.</name>
<url>http://my.repository.com/repo/path</url>
</mirror>
-->
<mirror>
<id>aliyunmaven</id>
<mirrorOf>*</mirrorOf>
<name>阿里云公共仓库</name>
<url>https://maven.aliyun.com/repository/public</url>
</mirror>
</mirrors>
加入环境变量
为了方便使用 maven,我们可以将 bin 目录加入到系统环境变量 PATH 当中,可以直接在控制台中启动。
IntelliJ IDEA
- 目前最流行的 Java 集成开发工具
- 官网:www.jetbrains.com/idea
导入和打开的区别:
用 idea 创建的项目可以直接打开,用 eclipse 创建的项目需要导入。
配置 Maven
创建项目
创建好项目,右键运行程序。
Spring Initializr
- 创建 Spring Boot 项目的引导工具。
- 按照功能需求归类,只要把相关的功能搜一下,就能把相关依赖的包下载下来,底层是基于 maven 的。Maven 仓库推荐
- 官网:https://start.spring.io/
进入官网选择好功能,点击按钮生成压缩包,解压后在 IDEA 中打开项目:
springboot 内置 tomcat。
Spring Boot 入门示例
-
Spring Boot 核心作用:
- 起步依赖(一个依赖其实是很多包的组合)
- 自动配置,几乎不用做配置就能跑起来程序
- 端点监控
一个简单的处理客户端请求案例
在上面的 spring initializr 解压的 community 项目里面新建一个 controller 包,新建一个 AlphaController
类。
package com.nowcoder.community.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* @author shkstart
* @create 2023-07-12 17:33
*/
@Controller
@RequestMapping("/alpha")
public class AlphaController {
@RequestMapping("/hello")
@ResponseBody
public String sayHello(){
return "Hello Spring Boot.";
}
}
修改 tomcat 的端口
加入以下两行:
server.port=8080
server.servlet.context-path=/community
访问地址:http://localhost:8080/community/alpha/hello
Spring 入门
spring 全家桶
- Spring Framework
- Spring Boot
- Spring Cloud
- Spring Cloud Data Flow
Spring Framework 介绍
-
Spring Core
- loC、AOP
-
Spring Data Access
- -Transactions、Spring MyBatis
-
Web Servlet
- -Spring MVC
-
Integration
-Email、Scheduling、AMQP、Security
演示 IOC 的使用
正常运行是运行的 CommunityApplication
启动类,但是我们希望在测试代码中的运行环境是和启动类一样的,因此我们需要在测试类上加注解 @ContextConfiguration(classes = CommunityApplication.class)
,并实现 ApplicationContextAware
接口,重写下面的 setApplicationContext
方法。
package com.nowcoder.community;
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;
@Test
void contextLoads() {
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext=applicationContext;
}
//测试传过来的这个spring容器是不是存在
@Test
public void testApplication(){
System.out.println(applicationContext);
}
}
新建一个 dao
包,在其下面新建一个 AlaphaDao
接口,写一个查询数据的 select
方法:
package com.nowcoder.community.dao;
/**
* @author shkstart
* @create 2023-07-13 10:41
*/
public interface AlphaDao {
String select();
}
接着,再新建一个 AlaphaDao
接口的 AlphaDaoHibernateImpl
实现类,如果没有加 @Repository
注解,则 spring 装配不了 bean 到容器里面,当 Spring 无法将 bean 装配到容器中时,意味着你无法通过 Spring 容器来获取该 bean 的实例。这可能会导致你无法访问数据库,因为通常数据库连接和操作是通过 Spring 容器中的 bean 来完成的。
package com.nowcoder.community.dao;
import org.springframework.stereotype.Repository;
/**
* @author shkstart
* @create 2023-07-13 10:44
*/
@Repository
public class AlphaDaoHibernateImpl implements AlphaDao{
@Override
public String select() {
return "Hibernate";
}
}
在测试类下这个方法,我们可以通过容器获取到 bean。
//测试传过来的这个spring容器是不是存在,通过这个容器获取到bean,再使用它的方法。
@Test
public void testApplication(){
System.out.println(applicationContext);
AlphaDao alphaDao = applicationContext.getBean(AlphaDao.class);
System.out.println(alphaDao.select());
}
假设这样的一个场景:我们使用的 hibernate 已经被淘汰了,mybatis 更有优势,需要将 hibernate 替换成 mybatis 的话,我们该怎么处理呢?
我们现在创建另外一个 AlphaDao
的实现类 AlphaDaoMyBatisImpl
,此时就有两个 AlphaDao 的实现类了,那么在测试类下的那个 applicationContext
获取到的对象就有歧义,不知道获取到的哪个,因此我们还需要加上 @Primary
注解,保证在装配 bean 时优先级最高。
package com.nowcoder.community.dao;
import org.springframework.stereotype.Repository;
/**
* @author shkstart
* @create 2023-07-13 10:57
*/
@Repository
public class AlphaDaoMyBatisImpl implements AlphaDao{
@Override
public String select() {
return "Mybatis";
}
}
使用 ApplicationContext
获取对象的好处就出来了,我们所做的就只是加个注解,创建另外一个实现类,就更新了使用的对象。这样做依赖的就是接口,而不是这个 bean 本身,降低了 bean 之间的藕度。
但是现在因为 mybatis
的优先级最高,所以到的 bean 对象一直都是 mybatis
,而这个时候我们想要用 hibernate
该怎么做?
这里因为 AlphaDaoHibernateImpl
这个类名比较长,我们可以修改一下注解,自定义名字:@Repository("alphaHibernate")
。
通过 alphaDao = applicationContext.getBean("alphaHibernate", AlphaDao.class);
就可以获取到 hibernate。
spring 管理容器的方法
创建一个 service
包,处理业务功能。在其下面新建一个 AlphaService
类:
package com.nowcoder.community.service;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
/**
* @author shkstart
* @create 2023-07-14 9:47
*/
@Service
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");
}
}
在测试类下获取 service 这个 bean 类。
//获取service
@Test
public void testBeanManagement(){
AlphaService alphaService= applicationContext.getBean(AlphaService.class);
System.out.println(alphaService);
}
如果想在获取 bean 的时候不是单例对象,就加类上加一个 @Scope("prototype")
,每次就会获取一个新的实例,一般默认的是单例 @Scope("singletop")
。
下面进行测试:
//获取service
@Test
public void testBeanManagement(){
AlphaService alphaService= applicationContext.getBean(AlphaService.class);
System.out.println(alphaService);
alphaService= applicationContext.getBean(AlphaService.class);
System.out.println(alphaService);
}
上面的测试管理的是自己写的类,但是有时候需要管理第三方打包的 jar 包里面的类该怎么处理呢?这个时候已经不能像刚刚所作的那样,直接加一些注解,它已经封装到 jar 包了。
一般做法是写一个配置类,再通过配置类 bean 注解进行声明解决这个问题。
新建一个 config
包,在其下面新建 AlphaConfig
类:
package com.nowcoder.community.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.text.SimpleDateFormat;
/**
* @author shkstart
* @create 2023-07-14 10:06
*/
@Configuration
public class AlphaConfig {
//这个方法的作用就是将返回的对象装配到容器里
@Bean
public SimpleDateFormat simpleDateFormat(){
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
}
在测试类下写一个方法:
//主动从容器中获取,拿到bean去用
@Test
public void testBeanConfig(){
SimpleDateFormat simpleDateFormat = applicationContext.getBean(SimpleDateFormat.class);
System.out.println(simpleDateFormat.format(new Date()));
}
这还是一种比较笨拙的方式,在 IOC 里面有一种思想叫依赖注入。请看下面的代码:
@Autowired
private AlphaDao alphaDao;
这两行代码的意思是 @Autowired
这个注解将 AlphaDao
注入给了这个 alphaDao
属性。
这个 alphadao
如果我们希望注入的是 hibernate
,加上 @Qualifier("alphaHibernate")
注解就可以了。
我们一般在 service
注入 dao
,在 controller
下注入 service
。
Spring MVC 入门
HTTP
- HyperText Transfer Protocol
- 用于传输 HTML 等内容的应用层协议
- 规定了浏览器和服务器之间如何通信, 以及通信时的数据格式。
了解协议的网站:
https://www.ietf.org/ 这个是官网,但是解释都太过于学术。
developer.mozilla.org/zh-cn/ 火狐浏览器出的一个网站,有对 http 详细概述的内容,清晰易懂。
Spring MVC
-
三层架构
- 表现层、业务层、数据访问层
-
MVC
- Model:模型层
- View:视图层
- Controller:控制层
-
核心组件
- 前端控制器:DispatcherServlet
这三者是如何协作的呢?MVC 主要解决的是表现层的问题,当浏览器发送请求访问服务器的时候,首先访问的是 Controller 控制器,接收请求体中的数据,调用业务层处理,然后将数据封装到 Model,传给视图层 View,视图层利用 Model 数据生成一个 html,然后返回给浏览器,再进行渲染。
这些层的调用都是由 DispatcherServlet
调用的,基于 Spring 容器。
Thymeleaf
-
模板引擎
- 生成动态的 HTML。
-
Thymeleaf
- 倡导自然模板,即以 HTML 文件为模板。
-
常用语法
- 标准表达式、判断与循环、模板的布局。
spring 配置类
在 application.properties
加入一行代码:spring.thymeleaf.cache=false
这行代码的作用就是将模板的缓存给关掉,在开发的情况下打开网页就不会还留存上次的数据,用不着自己手动清理。
实际上是给一个配置类注入注解,这个 ThymeleafConfiguration
类又是在给 ThymeleafProperties
这个类进行配置。
演示
处理 get 请求
在 AlphaController
类下加入下面的方法,这是最笨拙的底层的处理方法。
/**
* 不加注解的,最笨拙的方式。
* @param httpServletRequest
* @param httpServletResponse
*/
@RequestMapping("/http")
public void http(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse){
//获取请求数据
System.out.println(httpServletRequest.getMethod());
System.out.println(httpServletRequest.getServletPath());
Enumeration<String> names = httpServletRequest.getHeaderNames();
//获取消息头
while (names.hasMoreElements()){
String name=names.nextElement();//请求体的key
String value=httpServletRequest.getHeader(name);
System.out.println(name+":"+value);
}
//获取请求体的数据
System.out.println(httpServletRequest.getParameter("code"));
//返回响应数据
httpServletResponse.setContentType("text/html;charset=utf-8");
try{
PrintWriter writer=httpServletResponse.getWriter();
writer.write("<h1>牛客网</h1>");
}catch (IOException e){
e.printStackTrace();
}
}
现在我们演示一下简便的处理方式:
//Get请求
// /students?current=1&limit=20,这里指定用get请求
@RequestMapping(path="/students",method= RequestMethod.GET)
@ResponseBody
public String getStudents(
@RequestParam(name="current",required=false,defaultValue = "1") int current,
@RequestParam(name="limit",required=false,defaultValue = "10") int limit){
System.out.println(current);
System.out.println(limit);
return "some students";
}
-
@RequestMapping(path="/students",method= RequestMethod.GET)
这一行指定了映射参数和请求方法 -
@RequestParam(name="current",required=false,defaultValue = "1") int current
通过这个注解,我们可以设置参数名称,是否必须,和其默认值。 -
http://localhost:8080/community/alpha/students
请求下我们没写参数,获取到的是默认值。
有两种传参的方法,上面演示的是?key=value 这种方式,下面是将参数放到路径当中。
// /student/123 后面的123是id值,是动态的,我们必须要使用{id}接收请求,加上@PathVariable用来给参数id赋值
@RequestMapping(path="/student/{id}",method =RequestMethod.GET)
@ResponseBody
public String getStudent(@PathVariable("id") int id ){
System.out.println(id);
return "a student";
}
访问静态页面
在 static
创建 html
文件夹,创建一个 student
的 HTML 文件:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>增加学生</title>
</head>
<body>
<form method="post" action="/community/alpha/student">
<p>
姓名:<input type="text" name="name">
</p>
<p>
年龄:<input type="text" name="age">
</p>
<p>
<input type="submit" value="保存">
</p>
</form>
</body>
</html>
访问静态页面:
处理 post 请求
在 AlphaController
类下测试:
// Post请求
@RequestMapping(path="/student",method = RequestMethod.POST)
@ResponseBody
public String saveStudent(String name,int age){
System.out.println(name);
System.out.println(age);
return "success";
}
从刚刚那个静态网页下输入数据,点击保存就发送请求并跳转到第二个页面。
控制台显示的内容:
响应一个动态的 html 数据
在 AlphaController
类下测试:
//响应html数据
//通过model和view数据生成动态的html
@RequestMapping(path="/teacher",method = RequestMethod.GET)
public ModelAndView getTeacher(){
ModelAndView mav= new ModelAndView();
//模板的内容
mav.addObject("name","张三");
mav.addObject("age",30);
mav.setViewName("/demo/view");//模板放在templates下,参数里面就是传它的下级路径。
return mav;
}
mav.setViewName("/demo/view");
此时这个路径还不存在,我们还需要去 templates
文件夹下创建相应的 demo
文件夹和 view
这个 html 文件。
view.html 的内容:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Teacher</title>
</head>
<body>
<p th:text="${name}"></p>
<p th:text="${age}"></p>
</body>
</html>
-
<p th:text="${name}"></p>
在模板引擎 thymeleaf 渲染网页的时候会获取变量放到标签 p 里面。
还有另外一种方式可以响应网页,接下来是演示:
@RequestMapping(path = "/school",method = RequestMethod.GET)
public String getSchool(Model model){
model.addAttribute("name","北京大学");
model.addAttribute("age",80);
return "/demo/view";
}
这个方法把 model 装到参数里,把 view 返回给 dispatcherServelet,model 和 view 两个都持有,可以获得。
而前一种方法则是 model 和 view 统一封装到一个对象里面,效果是一样的。显然第二种方式更简单,最好用这种方式。
响应 JSON 数据
异步请求:就是网页不刷新,但是背后发送了一个请求,访问了数据库。
@RequestMapping(path="/emp",method = RequestMethod.GET)
@ResponseBody
public Map<String,Object> getEmp(){
Map<String,Object> emp=new HashMap<>();
emp.put("name","张三");
emp.put("age",23);
emp.put("salary",8000.00);
return emp;
}
返回多组数据:
//返回多组员工数据
@RequestMapping(path="/emps",method = RequestMethod.GET)
@ResponseBody
public List<Map<String,Object>> getEmps(){
List<Map<String,Object>> list=new ArrayList<>();
Map<String,Object> emp=new HashMap<>();
emp.put("name","张三");
emp.put("age",23);
emp.put("salary",8000.00);
list.add(emp);
emp=new HashMap<>();
emp.put("name","王五");
emp.put("age",28);
emp.put("salary",10000.00);
list.add(emp);
return list;
}
Mybatis 入门
安装数据库
-
安装 MySQL Server
-
安装 MySQL Workbench
-
官网:
在官网下载好这两个软件,从老师给的资料 community-init-sql-1.5
中复制文件 my.ini
到 mysql 根目录下。
然后修改其中的参数 basedir 为 MySQL 的路径:
配置环境变量
数据库的使用
右击管理员运行命令行,进行初始化:mysqld --initialize --console
安装 mysql 服务:mysqld install
启动服务:net start mysql
访问 mysql:mysql -uroot -p
修改临时密码:alter user root@localhost identified by 'password'
mysql 语句
新建数据库:create database community;
显示数据库:show databases;
使用数据库:use community
导入 sql 脚本:source 路径/文件
导入 init_data.sql
和 init_schema.sql
两个文件。
安装数据库客户端
点击这个,再右击 Edit connection,设置密码和访问库。
设置字体:
取消安全更新:
mybatis
MyBatis
-
核心组件
- SqISessionFactory:用于创建 SqISession 的工厂类。
- SqISession: MyBatis 的核心组件,用于向数据库执行 SQL。
- 主配置文件:XML 配置文件,可以对 MyBatis 的底层行为做出详细的配置。
- Mapper 接口:就是 DAO 接口,在 MyBatis 中习惯性的称之为 Mapper。
- Mapper 映射器:用于编写 SQL,并将 SQL 和实体类映射的组件,采用 XML、注解均可实现。
-
官网
使用 MyBatis 对用户表进行 CRUD 操作
从 maven 仓库获取 mysql
的 maven 坐标,复制到 pom.xml 下:
<!-- https://mvnrepository.com/artifact/com.mysql/mysql-connector-j -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.33</version>
</dependency>
获取 myatis
的坐标:
<!-- https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.1</version>
</dependency>
在 springboot 项目文件 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=2004
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
在加一行 logging.level.com.nowcoder.community=debug
方便后续进行调试,在查找数据库的时候会很详细的展示 sql 语句结果提示。
在 resources
包下新建一个 mapper
文件夹,在 项目包下
新建 entity包
,在 entity
下新建一个 User
类。
package com.nowcoder.community.entity;
import java.util.Date;
/**
* @author 008
* @create 2023-07-15 15:11
*/
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;
}
按快捷键 alt+insert
选择 getter and setter
,为所有属性生成读写器,再按快捷键生成一个 toString
方法。
在 dao
层生成一个 UserMapper
接口:
package com.nowcoder.community.dao;
import com.nowcoder.community.entity.User;
import org.apache.ibatis.annotations.Mapper;
/**
* @author 008
* @create 2023-07-15 15:16
*/
@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);
}
不知道为啥这里有一点小问题,会报以下错误:
org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.binding.BindingException: Parameter 'status' not found. Available parameters are [arg1, arg0, param1, param2]
搜到的解决方案是在参数前加上相应的 @Param("id")
注解,传入多个参数,就会报上面的错误。
在 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="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>
通过这上面的代码,我们定义来 sql
标签,是为了方便后续的代码复用。
<sql id="insertFields">
username, password, salt, email, type, status, activation_code, header_url, create_time
</sql>
想要在 sql 语句中引用这一段,用 include
标签:
<select id="selectById" resultType="User">
select <include refid="selectFields"></include>
from user
where id = #{id}
</select>
为了验证上面的方法有没有问题,我们写一个测试类:
package com.nowcoder.community;
import com.nowcoder.community.dao.UserMapper;
import com.nowcoder.community.entity.User;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.Date;
@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
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("nowcoder101@sina.com");
System.out.println(user);
}
@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://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);
}
}
开发社区首页
开发流程
-
开发流程
- 1 次请求的执行过程
-
分步实现
- 开发社区首页,显示前 10 个帖子
- 开发分页组件,分页显示所有的帖子
-
分页查询
在 entity
下新建 DiscussPost
类:
package com.nowcoder.community.entity;
import java.util.Date;
/**
* @author 008
* @create 2023-07-15 16:36
*/
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;
@Override
public String toString() {
return "DiscussPost{" +
"id=" + id +
", userId=" + userId +
", title='" + title + '\'' +
", content='" + content + '\'' +
", type=" + type +
", status=" + status +
", createTime=" + createTime +
", commentCount=" + commentCount +
", score=" + 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;
}
private double score;
public DiscussPost() {
}
}
在 dao
下创建 DiscussPostMapper
接口:
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;
/**
* @author 008
* @create 2023-07-15 16:48
*/
@Mapper
public interface DiscussPostMapper {
/**
* 查询评论(带分页信息)
* @param userId
* @param offset 起始行号
* @param limit 每页显示多少条数据
* @return
*/
List<DiscussPost> selectDiscussPosts(int userId,int offset,int limit);
//获取帖子总数
//@Param注解用于给参数取别名
//如果只有一个参数,并且在<if>里使用,则必须加别名
int selectDiscussPostRows(@Param("userId") int userId);
}
在 mapper
下创建一个 DiscussPostMapper.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>
在测试类下写方法:
@Test
public void testSelectPosts(){
List<DiscussPost> list=discussPostMapper.selectDiscussPosts(149,0,10);
for (DiscussPost post:list){
System.out.println(post);
}
int rows=discussPostMapper.selectDiscussPostRows(0);
System.out.println(rows);
}
即便是 service 很简单,也要在 controller 下调用,也是为了保证各层的安全性,方便后续使用。
在 service
下新建 DiscussPostService
类:
package com.nowcoder.community.service;
import com.nowcoder.community.dao.DiscussPostMapper;
import com.nowcoder.community.entity.DiscussPost;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @author 008
* @create 2023-07-15 21:50
*/
@Service
public class DiscussPostService {
@Autowired
private DiscussPostMapper discussPostMapper;
/**
* 查询某页的数据/分页查询
* @param userId
* @param offset
* @param limit
* @return
*/
public List<DiscussPost> findDiscussPosts(int userId,int offset,int limit){
return discussPostMapper.selectDiscussPosts(userId, offset, limit);
}
/**
* 查询发言的信息总数
* @param userId
* @return
*/
public int findDiscussPostRows(int userId){
return discussPostMapper.selectDiscussPostRows(userId);
}
}
我们在 DiscussPost
下有一个 userId
外键,如果要在网页上显示数据,不可能就显示 Id,而是要显示用户名称。有两种方式:
- 在
service
下关联查询用户,同时查询两种数据。 - 单独的查询
DiscussPost
,针对每一项数据查询 User,将两者组合在一起返回给页面。这种方式在使用 redis 时会很方便,也更直观。
这里采用第二种方式:
在 service
下新建 UserService
类:
package com.nowcoder.community.service;
import com.nowcoder.community.dao.UserMapper;
import com.nowcoder.community.entity.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @author 008
* @create 2023-07-15 22:19
*/
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public User findUserById(int id){
return userMapper.selectById(id);
}
}
从资源上复制静态页面 css
、img
、js
到 static
下,将 site
、mail
、index.html
复制到 templates
下。
开发首页
在 controller
下创建 HomeController
类:
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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author 008
* @create 2023-07-15 22:31
*/
@Controller
public class HomeController {
@Autowired
private DiscussPostService discussPostService;
@Autowired
private UserService userService;
/**
* 获取首页数据:User、DiscussPost
* @param model
* @return
*/
@RequestMapping(path="/index",method = RequestMethod.GET)
public String getIndexPage(Model model){
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
model.addAttribute("discussPosts",discussPosts);
return "/index";
}
}
修改相对路径的查找
修改 index.html
的内容,使用 thymeleaf 引擎,即使在相对路径的情况下,也会在 static
下查找内容:
<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="icon" href="https://static.nowcoder.com/images/logo_87_87.png"/>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" crossorigin="anonymous">
<link rel="stylesheet" th:href="@{/css/global.css}" />
<title>牛客网-首页</title>
- 第 2 行加上
xmlns:th="http://www.thymeleaf.org"
使用引擎 - 第 8 行修改成
th:href="@{/css/global.css}"
在 static 文件夹下查找
修改末尾处的两行数据:
<script th:src="@{/js/global.js}"></script>
<script th:src="@{/js/index.js}"></script>
处理首页内容的帖子列表部分
删除多余的 li,只保留一个 li。修改 li 为动态数据,方便后续遍历。
修改 index.html
:
<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="icon" href="https://static.nowcoder.com/images/logo_87_87.png"/>
<link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/4.3.1/css/bootstrap.min.css" crossorigin="anonymous">
<link rel="stylesheet" th:href="@{/css/global.css}" />
<title>牛客网-首页</title>
</head>
<body>
<div class="nk-container">
<!-- 头部 -->
<header class="bg-dark sticky-top">
<div class="container">
<!-- 导航 -->
<nav class="navbar navbar-expand-lg navbar-dark">
<!-- logo -->
<a class="navbar-brand" href="#"></a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<!-- 功能 -->
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav mr-auto">
<li class="nav-item ml-3 btn-group-vertical">
<a class="nav-link" href="index.html">首页</a>
</li>
<li class="nav-item ml-3 btn-group-vertical">
<a class="nav-link position-relative" href="site/letter.html">消息<span class="badge badge-danger">12</span></a>
</li>
<li class="nav-item ml-3 btn-group-vertical">
<a class="nav-link" href="site/register.html">注册</a>
</li>
<li class="nav-item ml-3 btn-group-vertical">
<a class="nav-link" href="site/login.html">登录</a>
</li>
<li class="nav-item ml-3 btn-group-vertical dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<img src="http://images.nowcoder.com/head/1t.png" class="rounded-circle" style="width:30px;"/>
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
<a class="dropdown-item text-center" href="site/profile.html">个人主页</a>
<a class="dropdown-item text-center" href="site/setting.html">账号设置</a>
<a class="dropdown-item text-center" href="site/login.html">退出登录</a>
<div class="dropdown-divider"></div>
<span class="dropdown-item text-center text-secondary">nowcoder</span>
</div>
</li>
</ul>
<!-- 搜索 -->
<form class="form-inline my-2 my-lg-0" action="site/search.html">
<input class="form-control mr-sm-2" type="search" aria-label="Search" />
<button class="btn btn-outline-light my-2 my-sm-0" type="submit">搜索</button>
</form>
</div>
</nav>
</div>
</header>
<!-- 内容 -->
<div class="main">
<div class="container">
<div class="position-relative">
<!-- 筛选条件 -->
<ul class="nav nav-tabs mb-3">
<li class="nav-item">
<a class="nav-link active" href="#">最新</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">最热</a>
</li>
</ul>
<button type="button" class="btn btn-primary btn-sm position-absolute rt-0" data-toggle="modal" data-target="#publishModal">我要发布</button>
</div>
<!-- 弹出框 -->
<div class="modal fade" id="publishModal" tabindex="-1" role="dialog" aria-labelledby="publishModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="publishModalLabel">新帖发布</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<form>
<div class="form-group">
<label for="recipient-name" class="col-form-label">标题:</label>
<input type="text" class="form-control" id="recipient-name">
</div>
<div class="form-group">
<label for="message-text" class="col-form-label">正文:</label>
<textarea class="form-control" id="message-text" rows="15"></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="publishBtn">发布</button>
</div>
</div>
</div>
</div>
<!-- 提示框 -->
<div class="modal fade" id="hintModal" tabindex="-1" role="dialog" aria-labelledby="hintModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="hintModalLabel">提示</h5>
</div>
<div class="modal-body" id="hintBody">
发布完毕!
</div>
</div>
</div>
</div>
<!-- 帖子列表 -->
<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:utext="${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>
<!-- 分页 -->
<nav class="mt-5">
<ul class="pagination justify-content-center">
<li class="page-item"><a class="page-link" href="#">首页</a></li>
<li class="page-item disabled"><a class="page-link" href="#">上一页</a></li>
<li class="page-item active"><a class="page-link" href="#">1</a></li>
<li class="page-item"><a class="page-link" href="#">2</a></li>
<li class="page-item"><a class="page-link" href="#">3</a></li>
<li class="page-item"><a class="page-link" href="#">4</a></li>
<li class="page-item"><a class="page-link" href="#">5</a></li>
<li class="page-item"><a class="page-link" href="#">下一页</a></li>
<li class="page-item"><a class="page-link" href="#">末页</a></li>
</ul>
</nav>
</div>
</div>
<!-- 尾部 -->
<footer class="bg-dark">
<div class="container">
<div class="row">
<!-- 二维码 -->
<div class="col-4 qrcode">
<img src="https://uploadfiles.nowcoder.com/app/app_download.png" class="img-thumbnail" style="width:136px;" />
</div>
<!-- 公司信息 -->
<div class="col-8 detail-info">
<div class="row">
<div class="col">
<ul class="nav">
<li class="nav-item">
<a class="nav-link text-light" href="#">关于我们</a>
</li>
<li class="nav-item">
<a class="nav-link text-light" href="#">加入我们</a>
</li>
<li class="nav-item">
<a class="nav-link text-light" href="#">意见反馈</a>
</li>
<li class="nav-item">
<a class="nav-link text-light" href="#">企业服务</a>
</li>
<li class="nav-item">
<a class="nav-link text-light" href="#">联系我们</a>
</li>
<li class="nav-item">
<a class="nav-link text-light" href="#">免责声明</a>
</li>
<li class="nav-item">
<a class="nav-link text-light" href="#">友情链接</a>
</li>
</ul>
</div>
</div>
<div class="row">
<div class="col">
<ul class="nav btn-group-vertical company-info">
<li class="nav-item text-white-50">
公司地址:北京市朝阳区大屯路东金泉时代3-2708北京牛客科技有限公司
</li>
<li class="nav-item text-white-50">
联系方式:010-60728802(电话) admin@nowcoder.com
</li>
<li class="nav-item text-white-50">
牛客科技©2018 All rights reserved
</li>
<li class="nav-item text-white-50">
京ICP备14055008号-4
<img src="http://static.nowcoder.com/company/images/res/ghs.png" style="width:18px;" />
京公网安备 11010502036488号
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</footer>
</div>
<script src="https://code.jquery.com/jquery-3.3.1.min.js" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" crossorigin="anonymous"></script>
<script th:src="@{/js/global.js}"></script>
<script th:src="@{/js/index.js}"></script>
</body>
</html>
-
用
utext
的好处是防止有转移字符出现。 -
${map.user.headerUrl}
相当于map.get("user").getHeaderUrl()
。
封装分页信息
创建 Page
类:
package com.nowcoder.community.entity;
/**
* 封装分页相关的信息
* @author 008
* @create 2023-07-16 12:05
*/
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){//每页最多只能显示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;
}
/**
* 获取总页数
*/
public int getTotal(){
//rows/limit [+1]
if(rows%limit==0){//能被整除就不用+1
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;//不能超过总页数
}
}
再整合 page 到 controller 里面:
@RequestMapping(path="/index",method = RequestMethod.GET)
public String getIndexPage(Model model,Page page){
//方法调用前,SpringMVC会自动实例化Model和Page,并将Page注入Model
//所以,在thymeleaf中可以直接访问Page对象中的数据
//设置page数据
page.setRows(discussPostService.findDiscussPostRows(0));
page.setPath("/index");
//封装首页数据到一个Map里面
List<DiscussPost> list = discussPostService.findDiscussPosts(0, page.getOffset(), page.getLimit());
修改 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>
<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" href="#" 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>
-
th:href="@{${page.path}(current=1)}"
=/index?current=1 -
th:each="i:${#numbers.sequence(page.from,page.to)}"
这段代码 thymeleaf 的 numbers 工具类会生成 from-to 之间的连续数字,而每一项就是 i。 - 这上面实现了首页时无上一页、末页时无上一页、当前页点亮的功能。
项目调试技巧
响应状态码的含义
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status
重定向
以低耦合的方式进行网页的跳转。
服务端断点调试技巧
点击行号前这段区域加断点,用 debug 方式启动程序。
F8
逐行向下执行,F7
向内执行,F9
直接执行到下一个断点。
管理所有断点:
客户端 JS 断点调试技巧
右击网页,选择 检查
。
加断点:
选中变量监视值:
设置日志级别
输出日志
新建 LoggerTests
类:
package com.nowcoder.community;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
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");
}
}
测试方法:
设置存放日志的路径
在 application.properties
设置存放日志的路径:
logging.file.name=d:/mavenprojects/community/community.log
老师写的那种方法应该过时了,会报红线。
分类别存储日志:
从 所有素材和源码\第一章素材和源码\源码
复制 logback-spring.xml
到 resources
下,修改 LOG_PATH
对应的值。
git 版本控制
这里就不做多余的笔记了,我的笔记:https://blog.csdn.net/weixin_46066669/article/details/131581007
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于