01 环境搭建 +SSM 框架入门

本贴最后更新于 618 天前,其中的信息可能已经渤澥桑田

课程介绍

课程目标

  • 学会主流的 web 开发技术和框架
  • 积累一个真实的 web 项目的开发经验
  • 掌握热点面试题的答题策略

image

技术架构

  • 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
    • 远程仓库:中央仓库、镜像仓库、私服仓库
  • 官网:https://maven.apache.org/index.html

maven 命令快速入门

更改远程仓库

因为 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 仓库的配置指南

image

加入环境变量

为了方便使用 maven,我们可以将 bin 目录加入到系统环境变量 PATH 当中,可以直接在控制台中启动。

image

image

image

IntelliJ IDEA

导入和打开的区别:

用 idea 创建的项目可以直接打开,用 eclipse 创建的项目需要导入。

image

配置 Maven

image

创建项目

image

image

创建好项目,右键运行程序。

image

Spring Initializr

  • 创建 Spring Boot 项目的引导工具。
  • 按照功能需求归类,只要把相关的功能搜一下,就能把相关依赖的包下载下来,底层是基于 maven 的。Maven 仓库推荐
  • 官网:https://start.spring.io/

进入官网选择好功能,点击按钮生成压缩包,解压后在 IDEA 中打开项目:

image

image

image

springboot 内置 tomcat。

Spring Boot 入门示例

  • Spring Boot 核心作用:

    • 起步依赖(一个依赖其实是很多包的组合)
    • 自动配置,几乎不用做配置就能跑起来程序
    • 端点监控

一个简单的处理客户端请求案例

在上面的 spring initializr 解压的 community 项目里面新建一个 controller 包,新建一个 AlphaController​类。

image

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."; } }

image

修改 tomcat 的端口

image

加入以下两行:

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

官网:https://spring.io/

Spring Framework 介绍

  • Spring Core

    • loC、AOP
  • Spring Data Access

    • -Transactions、Spring MyBatis
  • Web Servlet

    • -Spring MVC
  • Integration
    -Email、Scheduling、AMQP、Security

image

演示 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); } }

image

新建一个 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"; } }

image

使用 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); }

image

image

如果想在获取 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); }

image

上面的测试管理的是自己写的类,但是有时候需要管理第三方打包的 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())); }

image

这还是一种比较笨拙的方式,在 IOC 里面有一种思想叫依赖注入。请看下面的代码:

@Autowired private AlphaDao alphaDao;

这两行代码的意思是 @Autowired​这个注解将 AlphaDao​注入给了这个 alphaDao​属性。

image

这个 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 详细概述的内容,清晰易懂。

image

image

image

Spring MVC

  • 三层架构

    • 表现层、业务层、数据访问层
  • MVC

    • Model:模型层
    • View:视图层
    • Controller:控制层
  • 核心组件

    • 前端控制器:DispatcherServlet

image

这三者是如何协作的呢?MVC 主要解决的是表现层的问题,当浏览器发送请求访问服务器的时候,首先访问的是 Controller 控制器,接收请求体中的数据,调用业务层处理,然后将数据封装到 Model,传给视图层 View,视图层利用 Model 数据生成一个 html,然后返回给浏览器,再进行渲染。

这些层的调用都是由 DispatcherServlet​调用的,基于 Spring 容器。

image

image

Thymeleaf

  • 模板引擎

    • 生成动态的 HTML。
  • Thymeleaf

    • 倡导自然模板,即以 HTML 文件为模板。
  • 常用语法

    • 标准表达式、判断与循环、模板的布局。
  • 官网:https://www.thymeleaf.org/

​​image​​

spring 配置类

application.properties​加入一行代码:​spring.thymeleaf.cache=false

这行代码的作用就是将模板的缓存给关掉,在开发的情况下打开网页就不会还留存上次的数据,用不着自己手动清理。

image

实际上是给一个配置类注入注解,这个 ThymeleafConfiguration 类又是在给 ThymeleafProperties​这个类进行配置。

image

演示

处理 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(); } }

image

​​image​​

现在我们演示一下简便的处理方式:

//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​请求下我们没写参数,获取到的是默认值。​image

有两种传参的方法,上面演示的是?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"; }

​​image​​

访问静态页面

image

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>

访问静态页面:

image​​

处理 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"; }

从刚刚那个静态网页下输入数据,点击保存就发送请求并跳转到第二个页面。

​​image

image

控制台显示的内容:image

响应一个动态的 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 里面。

image

还有另外一种方式可以响应网页,接下来是演示:

@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 统一封装到一个对象里面,效果是一样的。显然第二种方式更简单,最好用这种方式。

image

响应 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; }

image

返回多组数据:

//返回多组员工数据 @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; }

image

Mybatis 入门

安装数据库

在官网下载好这两个软件,从老师给的资料 community-init-sql-1.5​中复制文件 my.ini​到 mysql 根目录下。

image

然后修改其中的参数 basedir 为 MySQL 的路径:

image

配置环境变量

image

数据库的使用

右击管理员运行命令行,进行初始化:mysqld --initialize --console

image

安装 mysql 服务:mysqld install

image

启动服务:net start mysql

image

访问 mysql:mysql -uroot -p

image

修改临时密码:alter user root@localhost identified by 'password'

image

mysql 语句

新建数据库:create database community;

显示数据库:show databases;

使用数据库:use community​​

导入 sql 脚本:source 路径/文件

image

导入 init_data.sql​和 init_schema.sql​两个文件。

安装数据库客户端

点击这个,再右击 Edit connection,设置密码和访问库。

image

image

设置字体:

image

取消安全更新:

image

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 个帖子
    • 开发分页组件,分页显示所有的帖子
  • image

分页查询

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,而是要显示用户名称。有两种方式:

  1. service​下关联查询用户,同时查询两种数据。
  2. 单独的查询 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">&times;</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(电话)&nbsp;&nbsp;&nbsp;&nbsp;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 &nbsp;&nbsp;&nbsp;&nbsp; <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

重定向

image

以低耦合的方式进行网页的跳转。

服务端断点调试技巧

点击行号前这段区域加断点,用 debug 方式启动程序。

image

F8​逐行向下执行,F7​向内执行,F9​直接执行到下一个断点。

管理所有断点:

image

客户端 JS 断点调试技巧

右击网页,选择 检查​。

image

加断点:

image

选中变量监视值:

image

设置日志级别

Logback Home (qos.ch)

输出日志

新建 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"); } }

测试方法:

image

设置存放日志的路径

application.properties​设置存放日志的路径:

logging.file.name=d:/mavenprojects/community/community.log

老师写的那种方法应该过时了,会报红线。

分类别存储日志:

所有素材和源码\第一章素材和源码\源码​复制 logback-spring.xml​到 resources​下,修改 LOG_PATH​对应的值。

image

git 版本控制

这里就不做多余的笔记了,我的笔记:https://blog.csdn.net/weixin_46066669/article/details/131581007

  • SSM

    SpringMVC, Spring, MyBatis

    12 引用 • 35 回帖
1 操作
bleaach 在 2023-07-16 22:01:40 更新了该帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...