0%

SpringBoot学习笔记

1 springboot概述

1.1 概述

Spring Boot是Spring提供的一个子项目,用于快速构建Spring应用程序。

1.2 传统方式构建spring应用程序

1 导入依赖繁琐。
2 项目配置繁琐。

1.3 SpringBoot特性

1.3.1 起步依赖

本质上就是一个Maven坐标,整合了完成一个功能需要的所有坐标。(解决配置繁琐的问题)

1.3.2 自动配置

遵循约定大于配置的原则,在boot程序启动后,一些bean对象会自动注入到iloc容器,不需要手动声明,简化开发。(解决项目配置繁琐的问题)

1.3.3 其他特性

1 内嵌的Tomcat、Jetty(无需部署WAR文件)。
2 外部化配置。部署完毕后,需要修改,只需要修改项目外部配置文件,直接重新启动项目就可以。
3 不需要XML配置(properties/yml)。

2 sprintboot入门

需求:使用SpringBoot开发一个web应用,浏览器发起请求/hello后,给浏览器返回字符串”hello world~”。

1 创建Maven工程。
2 导入spring-boot-starter-web起步依赖。
3 编写Controller。

1
2
3
4
5
6
7
@RestController
public class HelloController {
@RequestMapping("/hello")
public String hello(){
return "Hello World~";
}
}

4 提供启动类。

1
2
3
4
5
6
7
//启动类
@SpringBootApplication
public class SpringbootSyqApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootSyqApplication.class, args);
}
}

3 sprintboot工程创建

手动创建SpringBoot工程
1 创建Maven工程。
2 引入依赖。
3 提供启动类。

4 springboot配置文件_基本使用

4.1 学习路径

4.1.1 基础篇

配置文件。
整合MyBatis。
Bean管理。
自动配置管理。
自定义starter。

4.1.2 实战篇

项目开发。
整合三方技术。
项目部署。

4.1.3 面试篇

面试题。
源码。

4.2 配置文件

1 properties配置文件。application.properties文件。

1
2
3
4
//修改端口号
server.port=9090
//修改虚拟目录
server.servlet.context-path=/start

2 yaml配置文件(实际开发中更常用)。application.yml(主要)/application.yaml。

5 yml配置信息书写和获取

5.1 三方技术配置信息

只需编写配置信息,起步依赖会自动获取配置信息。

5.2 自定义配置信息

不仅需要编写配置信息,还需要实现配置信息的获取代码。

5.3 yml配置信息书写

1 值前边必须有空格,作为分隔符。
2 使用空格作为缩进表示层级关系,相同的层级左侧对齐。

1
2
3
4
5
# 学生的爱好-数组
hobbies:
- 打篮球
- 打豆豆
- 打游戏

5.4 配置信息的获取

5.4.1 @Value

1
2
3
4
5
6
//获取yml文件中指定键对应的值。
@Value("${键名}")

@Value("${email.user}")
//发件人邮箱
public String user;

在使用@Value注解时,多个层级的键名使用.来连接。

5.4.2 ConfigurationProperties(prefix=”前缀”)

实体类的成员变量名与配置文件中的键名一致。

1
@ConfigurationProperties(prefix="email")

6 springboot整合mybatis

1 引入mybatis-spring-boot-starter依赖。pom.xml文件修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!--web起步依赖(springboot自带)-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--mybatis的起步依赖-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<!--mysql驱动依赖-->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>

2 yml文件数据库配置。数据库驱动,数据库链接,用户名,密码等。SpringBoot自动读取配置信息。

1
2
3
4
5
6
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/kongming
username: root
password: 181234

目标:查询User表指定id的数据,响应给浏览器。
Mapper编写:

1
2
3
4
5
6
@Mapper
public interface UserMapper {
// 通过id找到User
@Select("select * from user where id = #{id}")
public User findById(Integer id);
}

Service编写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface UserService {
public User findById(Integer id);
}

//把当前UserServiceImpl类的对象交给IOC容器
@Service
public class UserServiceImpl implements UserService {
//注入一个UserMapper对象实现数据库查询
@Autowired
private UserMapper userMapper;

@Override
public User findById(Integer id) {
//CTRL+I重写方法
return userMapper.findById(id);
}
}

Controller编写:

1
2
3
4
5
6
7
8
9
10
@RestController
public class UserController {
//注入一个Service对象
@Autowired
private UserService userService;
@RequestMapping("/findById")
public User findById(Integer id){
return userService.findById(id);
}
}

7 Bean扫描

标签:

1
<context:component-scan base-package="com.kongming">

注解:

1
@ComponentScan(basePackages="com.kongming")

在SpringBoot启动类的注解,实际上是一个组合注解:

1
2
3
4
5
6
7
8
9
//默认扫描添加了该注解的启动类所在的包及其子包。
@SpringBootApplication
//可以手动添加不在该包内的包
@ComponentScan(basePackages="com.kongming")

//等价于
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan

8 Bean注册

8Bean注册1
如果要注册的Bean对象来自于第三方(不是自定义的),是无法用@Component及衍生注解声明bean的。Spring提供了两个注解来解决问题:
1 @Bean
2 @Import

1
mvn install:install-file -Dfile=C:\SunYuQi\data\研一\工作\springboot资料\02_资料\02_Bean注册资料\common-pojo-1.0-SNAPSHOT.jar -DgroupId=cn.itcast -DartifactId=common-pojo -Dversion=1.0 -Dpackaging=jar
1
2
3
4
5
6
<!--第三方包导入-->
<dependency>
<groupId>cn.itcast</groupId>
<artifactId>common-pojo</artifactId>
<version>1.0</version>
</dependency>

8.1 @Bean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//启动类,不建议在启动类中
@Configuration
//建议放在启动类所在的包中
@SpringBootApplication
public class SpringbootSyqApplication {

public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(SpringbootSyqApplication.class, args);
Country country = context.getBean(Country.class);
System.out.println(country);
}
//注入Country对象
//将方法返回值交给IOC容器管理,成为IOC容器的bean对象
@Bean
public Country country(){
return new Country();
}
}

建议的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Configuration
public class CommonConfig {
//注入Country对象
@Bean
public Country country(){
return new Country();
}
//对象默认的名字是方法名
@Bean
public Province province(){
return new Province();
}

//修改Bean对象的默认名称
@Bean("aa")
//如果方法的内部需要使用到ioc容器中已经存在的bean对象,那么只需要在方法上声明即可,spring会自动的注入。
@Bean()
public Province province(){
System.out.println("province"+country());
return new Province();
}
}

```java
//启动类
@SpringBootApplication
public class SpringbootSyqApplication {

public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(SpringbootSyqApplication.class, args);
Country country = context.getBean(Country.class);
System.out.println(country);

System.out.println(context.getBean("province"));
}
}

8.2 @Import

使用@Import相当于手动扫描类。
1 导入配置类。
2 导入ImportSelector接口实现类。

1
2
3
4
5
6
7
8
9
10
11
12
13
//启动类
@SpringBootApplication
@Import(CommonConfig.class)
public class SpringbootSyqApplication {

public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(SpringbootSyqApplication.class, args);
Country country = context.getBean(Country.class);
System.out.println(country);

System.out.println(context.getBean("province"));
}
}
1
2
3
4
5
6
7
8
9
10
@Import(CommonImportSelector.class)

public class CommonImportSelector implements ImportSelector {
//springboot会自动调用selectimports方法
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
//从配置文件中读取
return new String[]{"config.CommonConfig"};
}
}

在配置文件中,每行一个类名。实现上述代码的注释部分,完整如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class CommonImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
//读取配置文件的内容
List<String> imports = new ArrayList<String>();
InputStream is = CommonImportSelector.class.getClassLoader().getResourceAsStream("common.imports");
BufferedReader br = new BufferedReader(new InputStreamReader(is));
//一行一行的读取内容
String line = null;
try {
while((line = br.readLine())!=null){
imports.add(line);
}
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
if(br!=null){
try {
br.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
return imports.toArray(new String[0]);
}
}
1
2
3
4
5
6
7
8
9
10
@Target(ElementType.TYPE)
//当前的注解可以在类上使用
@Retention(RetentionPolicy.RUNTIME)
//当前的注解会保留在运行时阶段
@Import(CommonImportSelector.class)
public @interface EnableCommonConfig {
}

//启动类中的使用方式
@EnableCommonConfig

9 注册条件

9.1 需要注册条件的原因

配置文件中配置:

1
2
3
country:
name: china
system: socialism

在java文件中读取配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Configuration
public class CommonConfig {
//注入Country对象
@Bean
public Country country(@Value("${country.name}") String name, @Value("${country.system}") String system){
Country country = new Country();
country.setName(name);
country.setSystem(system);
return country;
}
//对象默认的名字是方法名
@Bean()
public Province province(Country country){
System.out.println("province"+country);
return new Province();
}
}

我们希望达到的效果是,如果配置文件中配置了指定的信息,则注入,否则不注入。

9.2 注册条件

SpringBoot提供了设置注册生效条件的注解@Conditional.
9注册条件1

1
2
3
4
5
6
7
8
9
10
//注入Country对象
//如果配置文件中配置了指定的信息,则注入,否则不注入。
@ConditionalOnProperty(prefix = "country",name = {"name","system"})
@Bean
public Country country(@Value("${country.name}") String name, @Value("${country.system}") String system){
Country country = new Country();
country.setName(name);
country.setSystem(system);
return country;
}
1
2
3
4
5
6
//如果ioc容器中不存在country,则注入Province,否则不注入
@Bean()
@ConditionalOnMissingBean(Country.class)
public Province province(){
return new Province();
}
1
2
3
4
5
6
7
@Bean()
//如果当前环境中存在DispatcherServlet类,则注入Province,否则不注入
//如果当前引入了web起步依赖,则环境中有DispatcherServlet类,否则没有
@ConditionalOnClass(name="org.springframework.web.servlet.DispatcherServlet")
public Province province(){
return new Province();
}

10 自动配置原理

10.1 需要学习自动配置原理的原因

1 自定义starter。
2 面试。

10.2 自动配置体验

遵循约定大于配置的原则,在boot程序启动后,起步依赖中的一些bean对象会自动注入到ioc容器。程序引入spring-boot-starter-web起步依赖,启动后,会自动往ioc容器中注入DispatcherServlet。

1
mvn install:install-file -Dfile=C:\SunYuQi\data\研一\工作\springboot资料\02_资料\03_自动配置\common-pojo-2.0-SNAPSHOT.jar -DgroupId=cn.itcast -DartifactId=common-pojo -Dversion=2.0 -Dpackaging=jar

10.3 自动配置原理

在主启动类上添加了SpringBootApplication注解,这个注解组合了EnableAutoConfiguration注解。
EnableAutoConfiguration注解又组合了Import注解,导入了AutoConfigurationImportSelector类。
实现selectImports方法,这个方法经过层层调用,最终会读取META-INF目录下的后缀名为imports的文件,当然了,boot2.7以前的版本,读取的是spring.factories文件。
读取到全类名之后,会解析注册条件,也就是@Conditional及其衍生注解,把满足注册条件的Bean对象自动注入到IOC容器中。

11 自定义starter

11.1 场景

在实际开发中,经常会定义一些公共组件,提供给各个项目团队使用。而在SpringBoot的项目中,一般会将公共组件封装为SpringBoot的starter。
xxxx-autoconfigure <—自动配置功能
xxxx-starter <—依赖管理功能

11.2 自定义mybatis的starter

1 创建dmybatis-spring-boot-autoconfigure模块,提供自动配置功能,并自定义配置文件 META/INF/spring/xxx.imports.
2 创建dmybatis-spring-boot-stater模块,在starter中引入自动配置模块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//表示当前类是一个自动配置类
@AutoConfiguration
public class MybatisAutoConfig {
//SqlSessionFactoryBean
@Bean
public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource){
//把dataSource注入到sqlSessionFactoryBean就知道要连接的数据库
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
return sqlSessionFactoryBean;
}
//MapperScannerConfigure
@Bean
public MapperScannerConfigurer mapperScannerConfigurer(BeanFactory beanFactory){
MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
//设置扫描的包:启动类所在的包及其子包
List<String> packages = AutoConfigurationPackages.get(beanFactory);
//启动类只有一个,所以packages中只会有一个包,直接取出0号元素即可。
String p = packages.get(0);
mapperScannerConfigurer.setBasePackage(p);
//扫描的注释
mapperScannerConfigurer.setAnnotationClass(Mapper.class);
return mapperScannerConfigurer;
}
}
1
com.kongming.config.MybatisAutoConfig

pom文件中是定义好的autoconfig和autoconfig的依赖。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!--引入定义好的autoconfig-->
<dependency>
<groupId>com.kongming</groupId>
<artifactId>dmybatis-spring-boot-autoconfigure</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>3.3.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<version>3.3.1</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.14</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>

12 实战篇-开发模式和环境搭建

12.1 实战开发的技术栈

后台:
Validation做参数校验。
Mybatis做数据库操作。
Redis做缓存。
Junit做单元测试。
SpringBoot项目部署。
前端:
Vite Vue项目的脚手架。
Router路由。
Pina状态管理。
Element-Plus UI组件。

12.2 开发模式

接口文档:
1 路径和请求方式。
2 请求参数。
3 响应数据。

12.3 环境搭建

执行资料中的big_event.sql脚本,准备数据库表。
创建springboot工程,引入对应的依赖(web,mybatis,mysql驱动)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!--web依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--mybatis依赖-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<!--mysql驱动依赖-->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>

配置文件application.yml中引入mybatis的配置信息。

1
2
3
4
5
6
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/kongming
username: root
password: 181234

创建包结构,并准备实体类。
12开发模式和环境搭建1
其中pojo存放实体类,utils存放工具类。

13 实战篇-注册接口

用户模块需要开发的接口:
1 注册。
2 登录。
3 获取用户详细信息。
4 更新用户基本信息。
5 更新用户头像。
6 更新用户密码。

lowbok:在编译阶段为实体类自动生成setter,getter,toString方法
使用lowbok:
(1)在pom文件中引入依赖;
(2)在实体类上添加注解。

1
2
3
4
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
@Data
public class User {
private Integer id;//主键ID
private String username;//用户名
private String password;//密码
private String nickname;//昵称
private String email;//邮箱
private String userPic;//用户头像地址
private LocalDateTime createTime;//创建时间
private LocalDateTime updateTime;//更新时间
}

在其余实体类中都添加Data注解。在缺少构造方法的实体类中添加下列注解:

1
2
3
4
//无参数构造方法
@NoArgsConstructor
//全参数构造方法
@AllArgsConstructor

13.1 注册

明确需求->阅读接口文档->思路分析->开发->测试。

13.1.1 基本信息

请求路径:/user/register
请求方式:POST
接口描述:该接口用于注册新用户

13.1.2 请求参数

请求参数格式:x-www-form-urlencoded

请求参数说明:

参数名称 说明 类型 是否必须 备注
username 用户名 string 5~16位非空字符
password 密码 string 5~16位非空字符

请求数据样例:

1
username=zhangsan&password=123456

13.1.3 响应数据

响应数据类型:application/json

响应参数说明:

名称 类型 是否必须 默认值 备注 其他信息
code number 必须 响应码, 0-成功,1-失败
message string 非必须 提示信息
data object 非必须 返回的数据

响应数据样例:

1
2
3
4
5
{
"code": 0,
"message": "操作成功",
"data": null
}

13.2 代码编写

1 Controller部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@RestController
@RequestMapping("/user")
public class UserController {

//注入一个UserService对象
//在UserService部分要注入一个userService对象,使用@Service注解
@Autowired
private UserService userService;
@PostMapping("/register")
public Result register(String username, String password) {
//查询用户
User u = userService.findByUserName(username);
if(u == null){
//用户名没有占用
userService.register(username,password);
return Result.success();
}else{
//占用
return Result.error("用户名已被占用");
}
//注册
}
}

2 Service部分。

1
2
3
4
5
6
7
8
9
public interface UserService {
//CTRL+ALT转到对应UserService实体类
//ALT+ENTER生成方法
//根据用户名查询用户
User findByUserName(String username);

//注册
void register(String username, String password);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//在容器中注册当前类对象
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public User findByUserName(String username) {
User u = userMapper.findByUserName(username);
return u;
}

@Override
public void register(String username, String password) {
//密码要加密处理
String md5String = Md5Util.getMD5String(password);
//添加到数据库
userMapper.add(username,md5String);
}
}

3 Mapper部分。

1
2
3
4
5
6
7
8
9
10
@Mapper
public interface UserMapper {
//根据用户名查询用户
@Select("select * from user where username=#{username}")
User findByUserName(String username);
//添加
@Insert("insert into user(username,password,create_time,update_time)" +
"values (#{username},#{password},now(),now())")
void add(String username, String password);
}

14 实战篇-注册接口参数校验

在接口文档中,请求参数必须是5-16位的非空字符。后端必须保证前端传来的参数是符合要求的。

14.1 手动添加校验

在controller层添加校验。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@RestController
@RequestMapping("/user")
public class UserController {

//注入一个UserService对象
//在UserService部分要注入一个userService对象,使用@Service注解
@Autowired
private UserService userService;
@PostMapping("/register")
public Result register(String username, String password) {
if(username != null && username.length()>=5 && username.length()<=16 &&
password != null && password.length()>=5 && password.length()<=16) {
//查询用户
User u = userService.findByUserName(username);
if(u == null){
//用户名没有占用
//注册
userService.register(username,password);
return Result.success();
}else{
//占用
return Result.error("用户名已被占用");
}
}else{
return Result.error("参数不合法");
}
}
}

14.2 Spring Validation

上述参数校验代码过于繁琐,Spring提供一个参数校验框架,使用预定义的注解完成参数校验。
1 引入Spring Validation起步依赖。

1
2
3
4
5
<!--validation依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

2 在参数前面添加@Pattern注解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@RestController
@RequestMapping("/user")
@Validated
public class UserController {

//注入一个UserService对象
//在UserService部分要注入一个userService对象,使用@Service注解
@Autowired
private UserService userService;
@PostMapping("/register")
public Result register(@Pattern(regexp = "^\\S{5,16}$") String username, @Pattern(regexp = "^\\S{5,16}$") String password) {
//查询用户
User u = userService.findByUserName(username);
if(u == null){
//用户名没有占用
//注册
userService.register(username,password);
return Result.success();
}else{
//占用
return Result.error("用户名已被占用");
}
}
}

3 在Controller类上添加@Validation注解。

4 在全局异常处理器中处理参数校验失败的异常。参数校验失败异常处理。@Restxxxb表示所有返回值都会被自动转化为json字符串。

1
2
3
4
5
6
7
8
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public Result handleException(Exception e) {
e.printStackTrace();
return Result.error(StringUtils.hasLength(e.getMessage())?e.getMessage():"操作失败");
}
}

15 实战篇-登录主逻辑

15.1 基本信息

请求路径:/user/login
请求方式:POST
接口描述:该接口用于登录

15.2 请求参数

请求参数格式:x-www-form-urlencoded

请求参数说明:

参数名称 说明 类型 是否必须 备注
username 用户名 string 5~16位非空字符
password 密码 string 5~16位非空字符

请求数据样例:

1
username=zhangsan&password=123456

15.3 响应数据

响应数据类型:application/json

响应参数说明:

名称 类型 是否必须 默认值 备注 其他信息
code number 必须 响应码, 0-成功,1-失败
message string 非必须 提示信息
data string 必须 返回的数据,jwt令牌

响应数据样例:

1
2
3
4
5
{
"code": 0,
"message": "操作成功",
"data": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGFpbXMiOnsiaWQiOjUsInVzZXJuYW1lIjoid2FuZ2JhIn0sImV4cCI6MTY5MzcxNTk3OH0.pE_RATcoF7Nm9KEp9eC3CzcBbKWAFOL0IsuMNjnZ95M"
}

jwt令牌是一串字符串。

15.4 备注说明

用户登录成功后,系统会自动下发JWT令牌,然后在后续的每次请求中,浏览器都需要在请求头header中携带到服务端,请求头的名称为 Authorization,值为登录时下发的JWT令牌。
如果检测到用户未登录,则http响应状态码为401

15.5 代码实现

service和mapper使用用户注册中写好的即可。主要需要在controller中添加用户登录模块代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@PostMapping("/login")
public Result<String> login(@Pattern(regexp = "^\\S{5,16}$") String username, @Pattern(regexp = "^\\S{5,16}$") String password) {
//根据用户名查询用户
User loginUser = userService.findByUserName(username);
//判断该用户是否存在
if(loginUser == null){
return Result.error("用户名错误");
}
//判断密码是否正确。loginUser对象中的
if(Md5Util.getMD5String(password).equals(loginUser.getPassword())){
//登录成功
return Result.success("jwt token令牌..");
}
return Result.error("密码错误");
}

16 实战篇-登录认证引入

登录认证-在未登录的情况下,可以访问到其他资源。
令牌就是一段字符串。
1 承载业务数据,减少后续请求查询数据库的次数。
2 防篡改,保证信息的合法性和有效性。

17 实战篇-JWT令牌

17.1 JWT简介

全称:JSON Web Token
定义了一种简洁的,自包含的格式,用于通信双方以json数据格式安全的传输信息。
组成:

  • 第一部分:Header(头),记录令牌类型,签名算法等。例如{“alg”:”HS256”,”type”:”JWT”}。加密算法是用于防篡改。
  • 第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。例如{“id”:”1”,”username”:”Tom”}
    Base64:是一种基于64个可打印字符(A-Z a-z 0-9 +/)来表示二进制数据的编码方式。
  • 第三部分:Signature(签名),防止Token被篡改、确保安全性。将header、payload,并加入指定密钥,通过指定签名算法计算而来。

17.2 JWT-生成

1 引入pom依赖。

1
2
3
4
5
6
7
8
9
10
11
<!--java-jwt坐标-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>
<!--单元测试的坐标-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>

2 生成JWT。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class JwtTest {
@Test
public void testGen() {
Map<String, Object> claims = new HashMap<>();
claims.put("id",1);
claims.put("username","kongming");
//生成jwt的代码
String token = JWT.create()
.withClaim("user",claims)//添加载荷
.withExpiresAt(new Date(System.currentTimeMillis() + 1000*60*60*12))//添加过期时间12h
.sign(Algorithm.HMAC256("kongming"));//指定算法,配置密钥
System.out.println(token);
}
}

3 验证JWT

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void testParse(){
//定义字符串,模拟用户传过来的token
String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" +
".eyJ1c2VyIjp7ImlkIjoxLCJ1c2VybmFtZSI6ImtvbmdtaW5nIn0sImV4cCI6MTcyMTY1NzU5NH0" +
".o2AiTydAFdgI2U_NXnqiLhuEwUEQCPnl76IQSVxjMi4";
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("kongming")).build();
DecodedJWT decodedJWT = jwtVerifier.verify(token);//验证token,生成一个解析后的JWT对象
Map<String, Claim> claims = decodedJWT.getClaims();
System.out.println(claims.get("user"));
}
//如果篡改了头部和载荷部分的数据,那么验证失败。
//如果密钥改了,验证失败。
//如果密钥过期了,验证失败。
  • JWT校验时使用的密钥签名,必须和生成JWT令牌时使用的密钥是配套的。
  • 如果JWT令牌解析校验时报错,则说明JWT令牌被篡改或失效了,令牌非法。

18 实战篇-登录认证-完成

在UserController部分生成JWT。

1
2
3
4
5
6
7
8
9
//判断密码是否正确。loginUser对象中的
if(Md5Util.getMD5String(password).equals(loginUser.getPassword())){
//登录成功
Map<String, Object> claims = new HashMap<>();
claims.put("id",loginUser.getId());
claims.put("username",loginUser.getUsername());
String token = JwtUtil.genToken(claims);
return Result.success(token);
}

在功能模块添加JWT验证部分。例如获取文章列表部分。

1
2
3
4
5
6
7
8
9
10
11
12
public Result<String> list(@RequestHeader(name = "Authorization")String token, HttpServletResponse response){
//在提供服务之前验证token
//CTRL+Alt+T 直接try catch
try {
Map<String, Object> claims = JwtUtil.parseToken(token);
return Result.success("所有的文章数据...");
} catch (Exception e) {
//http响应状态码为401
response.setStatus(401);
return Result.error("未登录");
}
}

使用拦截器统一完成JWT验证。
在工程目录下创建interceptors/LoginInterceptor.class文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//把当前拦截器对象注入到IOC容器中
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//令牌验证
String token = request.getHeader("Authorization");
//在提供服务之前验证token
//CTRL+Alt+T 直接try catch
try {
Map<String, Object> claims = JwtUtil.parseToken(token);
//放行
return true;
} catch (Exception e) {
//http响应状态码为401
response.setStatus(401);
//不放行
return false;
}
}
}

在工程目录下创建config/WebConfig.class文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class WebConfig implements WebMvcConfigurer {

//注册拦截器
@Autowired
private LoginInterceptor loginInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
//登录接口和注册接口不拦截
registry.addInterceptor(loginInterceptor).excludePathPatterns("/user/login","/user/register");
}
}

此时之前写在获取文章列表部分的登录验证代码可以注释掉了。

19 实战篇-获取用户详细信息

1
2
3
4
5
6
7
8
9
10
11
12
// token值的获取是通过注解实现的
@GetMapping("/userInfo")
public Result<User> userInfo(@RequestHeader(name = "Authorization")String token){
//根据用户名查询用户
//解析token
Map<String,Object> map = JwtUtil.parseToken(token);
// ALT+回车补全等号左边内容
String username = map.get("username").toString();

User user = userService.findByUserName(username);
return Result.success(user);
}

防止springBoot把用户的密码返回给前端。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import com.fasterxml.jackson.annotation.JsonIgnore;
//必须是这个包里的才行
//lowbok 在编译阶段为实体类自动生成setter,getter,toString方法
//使用lowbok:1)在pom文件中引入依赖;2)在实体类上添加注解
@Data
public class User {
private Integer id;//主键ID
private String username;//用户名
@JsonIgnore //让springmvc把当前对象转换成json字符串的时候忽略password,最终的json字符串中就没有password这个属性了
private String password;//密码
private String nickname;//昵称
private String email;//邮箱
private String userPic;//用户头像地址
private LocalDateTime createTime;//创建时间
private LocalDateTime updateTime;//更新时间
}

当前返回给前端的数据中创建时间和更新时间为空的原因是因为在数据库中这两个字段是下划线命名,而在User类中是驼峰命名。

1
2
3
mybatis:
configuration:
map-underscore-to-camel-case: true # 开启驼峰命名和下划线命名的自动转换

20 实战篇-获取用户详细信息-ThreadLocal优化

在拦截器里写的解析token的代码,在userInfo方法中被再写了一次。在线程内实现代码复用,在拦截器里写的解析token代码,不会再出现在userInfo方法中。

ThreadLocal-提供线程局部变量。用来存取数据:set()/get()。使用ThreadLocal存储的数据,线程安全。使用示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ThreadLocalTest {

//表示这是一个测试类
@Test
public void testThreadLocalSetAndGet(){
//提供一个ThreadLocal对象
ThreadLocal tl = new ThreadLocal();
//开启两个线程
new Thread(()->{
tl.set("诸葛亮");
System.out.println(Thread.currentThread().getName()+": "+tl.get());
System.out.println(Thread.currentThread().getName()+": "+tl.get());
System.out.println(Thread.currentThread().getName()+": "+tl.get());
},"蓝色").start();

new Thread(()->{
tl.set("孔明");
System.out.println(Thread.currentThread().getName()+": "+tl.get());
System.out.println(Thread.currentThread().getName()+": "+tl.get());
System.out.println(Thread.currentThread().getName()+": "+tl.get());
},"绿色").start();
}
}

后端的三个组成是:Controller,Service,Dao
在过滤器中使用ThreadLocal的set方法记录userid,在Controller,Service,Dao中使用get方法获得userid。

在过滤器中存储。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//把当前拦截器对象注入到IOC容器中
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//令牌验证
String token = request.getHeader("Authorization");
//在提供服务之前验证token
//CTRL+Alt+T 直接try catch
try {
Map<String, Object> claims = JwtUtil.parseToken(token);
//把业务数据存储到ThreadLocal中
ThreadLocalUtil.set(claims);
//放行
return true;
} catch (Exception e) {
//http响应状态码为401
response.setStatus(401);
//不放行
return false;
}
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//清空ThreadLocal中的数据
ThreadLocalUtil.remove();
}
}

在userInfo中获取。(controller层中)

1
2
3
4
5
6
7
8
9
10
11
12
13
// token值的获取是通过注解实现的
@GetMapping("/userInfo")
public Result<User> userInfo(/*@RequestHeader(name = "Authorization")String token*/){
//根据用户名查询用户
//解析token
// Map<String,Object> map = JwtUtil.parseToken(token);
// ALT+回车补全等号左边内容
// String username = map.get("username").toString();
Map<String,Object> map = ThreadLocalUtil.get();
String username = map.get("username").toString();
User user = userService.findByUserName(username);
return Result.success(user);
}

21 实战篇-更新用户基本信息

在controller中新增updata方法。在Service层中新增updata方法。在DAO层中新增updata查询。

controller方法。

1
2
3
4
5
@PutMapping("/update")
public Result update(@RequestBody User user){
userService.update(user);
return Result.success();
}

service方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public interface UserService {
//CTRL+ALT转到对应UserService实体类
//ALT+ENTER生成方法
//根据用户名查询用户
User findByUserName(String username);

//注册
void register(String username, String password);

//更新
//具体方法在UserService实现类中实现
void update(User user);
}

//在容器中注册当前类对象
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;

@Override
public User findByUserName(String username) {
User u = userMapper.findByUserName(username);
return u;
}

@Override
public void register(String username, String password) {
//密码要加密处理
String md5String = Md5Util.getMD5String(password);
//添加到数据库
userMapper.add(username, md5String);
}

@Override
public void update(User user) {
//需要给updateTime属性赋值
user.setUpdateTime(LocalDateTime.now());
userMapper.update(user);
}

DAO层,添加update方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Mapper
public interface UserMapper {
//根据用户名查询用户
@Select("select * from user where username=#{username}")
User findByUserName(String username);
//添加
@Insert("insert into user(username,password,create_time,update_time)" +
"values (#{username},#{password},now(),now())")
void add(String username, String password);

//前面是表格的属性的名字,后面是变量名
@Update("update user set nickname=#{nickname},email=#{email},update_time=#{updateTime} where id=#{id}")
void update(User user);
}

22 实战篇-更新用户基本信息_参数校验

根据接口文件中对nickname和Email的要求,对更新的用户基本信息完成校验。在注册接口中,是通过添加pattern注解对参数完成校验。

1 在实体类的成员变量加上validation提供的注解。例如’@NotNull’。
NotNull:值不能为null。
NotEmpty:值不能为null,并且内容不为空。
Email:满足邮箱格式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//lowbok 在编译阶段为实体类自动生成setter,getter,toString方法
//使用lowbok:1)在pom文件中引入依赖;2)在实体类上添加注解
@Data
public class User {
@NotNull
private Integer id;//主键ID
private String username;//用户名
@JsonIgnore //让springmvc把当前对象转换成json字符串的时候忽略password,最终的json字符串中就没有password这个属性了
private String password;//密码
@NotEmpty
//1-10非空字符规则-很容易写错
@Pattern(regexp = "^\\S{1,10}$")
private String nickname;//昵称
@NotEmpty
@Email
private String email;//邮箱
private String userPic;//用户头像地址
private LocalDateTime createTime;//创建时间
private LocalDateTime updateTime;//更新时间
}

2 需要在具体使用该注解的函数参数部分添加@Validated注解。这样属性部分的注解(也就是上文中提到的那些)才能生效。

在controller层中。

1
2
3
4
5
@PutMapping("/update")
public Result update(@RequestBody @Validated User user){
userService.update(user);
return Result.success();
}

23 实战篇-更新用户头像

在controller层中增加updateAvatar方法。参数校验。对于avatarUrl是否是一个url地址进行校验。

1
2
3
4
5
@PatchMapping("updateAvatar")
public Result updateAvatar(@RequestParam String avatarUrl){
userService.updateAvatar(avatarUrl);
return Result.success();
}

在service层中增加updateAvatar方法。

1
2
3
4
5
6
7
8
9
10
11
12
//在userService类中
//更新头像
void updateAvatar(String avatarUrl);

//在userService实现类中
//id存放在threadLocal中
@Override
public void updateAvatar(String avatarUrl) {
Map<String,Object> map = ThreadLocalUtil.get();
Integer id = Integer.parseInt(map.get("id").toString());
userMapper.updateAvatar(avatarUrl,id);
}

在DAO层中增加更新头像的sql语句。

1
2
3
//updateTime通过now方法获取服务器上的时间
@Update("update user set user_pic=#{avatarUrl},update_time=now() where id=#{id}")
void updateAvatar(String avatarUrl,Integer id);

在用户相关接口更新JWT令牌的时候不要把Authorization:去掉了。

24 实战篇-更新用户密码

在Controller层中新增updatePwd方法,注意添加PatchMapping注解。我们需要申明一个Map类型的参数,用来接收前端提交的json参数。之前我们使用user实体对象来接收,是因为我们之前传进来的键名,刚好和user实体对象的属性名相同。但是现在我们的请求参数的键名是old_pwd,new_pwd,re_pwd,和实体对象的属性名没有办法相同了,于是申明一个Map类型的参数。MVC框架会自动帮我们把json数据转化成map集合对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// @RequestBody实现请求体里的json格式的数据转换为map对象
@PatchMapping("/updatePwd")
public Result updatePwd(@RequestBody Map<String,String> params){
//1 校验参数
//validation提供的注解无法满足参数校验的需求
String oldPwd = params.get("old_pwd");
String newPwd = params.get("new_pwd");
String rePwd = params.get("re_pwd");

if(!StringUtils.hasLength(oldPwd)||!StringUtils.hasLength(newPwd)||!StringUtils.hasLength(rePwd)){
return Result.error("缺少必要的参数");
}
//原密码是否正确
//调用userService根据用户名拿到原密码,再和old_pwd比对
Map<String,Object> map = ThreadLocalUtil.get();
String username = map.get("username").toString();
User loginUser = userService.findByUserName(username);
//获取的是加密后的密码
if(!loginUser.getPassword().equals(Md5Util.getMD5String(oldPwd))){
return Result.error("原密码填写不正确");
}

//newPwd和rePwd填写是否一样
if(!rePwd.equals(newPwd)){
return Result.error("两次填写的新密码不一样");
}
//2 调用service实现密码更新
userService.updatePwd(newPwd);
return Result.success();
}

在Service层中新增updatePwd方法完成密码更新。

1
2
3
4
5
6
7
8
9
10
11
//在userService类中
//更新密码
void updatePwd(String newPwd);

//在userService实现类中
@Override
public void updatePwd(String newPwd) {
Map<String,Object> map = ThreadLocalUtil.get();
Integer id = Integer.parseInt(map.get("id").toString());
userMapper.updatePwd(Md5Util.getMD5String(newPwd),id);
}

在DAO层中新增相应的sql语句。

1
2
@Update("update user set password=#{md5String},update_time=now() where id=#{id}")
void updatePwd(String md5String, Integer id);

25 实战篇-新增文章分类

在CategoryController中新增add方法。使用@RequestBody注解。
使用@PostMapping注解,并没有添加映射路径,后续会在Category类上添加路径\Category。
同时在新增文章分类的controller层上增加了参数校验。

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
@RequestMapping("/category")
public class CategoryController {

@Autowired
private CategoryService categoryService;
@PostMapping
public Result add(@RequestBody @Validated Category category) {
categoryService.add(category);
return Result.success();
}
}

在service层添加add方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//在CategoryService接口中
public interface CategoryService {
//新增分类
void add(Category category);
}
//在CategoryService实现类中
@Service
public class CategoryServiceImpl implements CategoryService {

@Autowired
private CategoryMapper categoryMapper;
@Override
public void add(Category category) {
//补充属性值
category.setCreateTime(LocalDateTime.now());
category.setUpdateTime(LocalDateTime.now());

Map<String,Object> map = ThreadLocalUtil.get();
Integer userid = Integer.parseInt(map.get("id").toString());
category.setCreateUser(userid);
categoryMapper.add(category);
}
}

在DAO层添加对应的sql语句。

1
2
3
4
5
6
7
@Mapper
public interface CategoryMapper {
//新增分类
@Insert("insert into category(category_name,category_alias,create_user,create_time,update_time) " +
"values(#{categoryName},#{categoryAlias},#{createUser},#{createTime},#{updateTime})")
void add(Category category);
}

26 实战篇-文章分类列表

和新增文章分类的请求路径都是”\category”,是通过请求方式的不同来区分这两个接口的。新增是POST,文章分类列表的查询是GET。
在CategoryController中新增list方法。

1
2
3
4
5
@GetMapping
public Result<List<Category>> list(){
List<Category> cs = categoryService.list();
return Result.success(cs);
}

在CategoryService中新增list方法。

1
2
3
4
5
6
7
8
9
10
11
//在categoryService接口中
//列表查询
List<Category> list();

//在categoryService实现类中
@Override
public List<Category> list() {
Map<String,Object> map = ThreadLocalUtil.get();
Integer id = Integer.parseInt(map.get("id").toString());
return categoryMapper.list(id);
}

在mapper层中执行对应的select语句。要根据create_user字段进行查询。

1
2
3
//查询所有
@Select("select * from category where create_user = #{id}")
List<Category> list(Integer id);

修改postman中的日期格式。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Data
public class Category {
private Integer id;//主键ID
@NotEmpty
private String categoryName;//分类名称
@NotEmpty
private String categoryAlias;//分类别名
private Integer createUser;//创建人ID
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;//创建时间
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;//更新时间
}

27 实战篇-获取文章分类详情

在categoryController中新增detail方法。

1
2
3
4
5
@GetMapping("/detail")
public Result<Category> detail(Integer id) {
Category c = categoryService.findById(id);
return Result.success(c);
}

在categoryService中新增findById方法。

1
2
3
4
5
6
7
8
9
10
//在categoryService接口中
//根据id查询分类信息
Category findById(Integer id);

//在categoryService实现类中
@Override
public Category findById(Integer id) {
Category c = categoryMapper.findById(id);
return c;
}

在categoryMapper中新增对应sql语句。

1
2
3
//根据id查询
@Select("select * from category where id = #{id}")
Category findById(Integer id);

28 实战篇-更新文章分类

请求路径和获取文章分类列表一样,但是请求方式与之(get)不同,为put。
在CategoryController中添加update方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@PutMapping
public Result update(@RequestBody @Validated Category category) {
categoryService.update(category);
return Result.success();
}

//在category类中新增注解
@Data
public class Category {
@NotNull
private Integer id;//主键ID
@NotEmpty
private String categoryName;//分类名称
@NotEmpty
private String categoryAlias;//分类别名
private Integer createUser;//创建人ID
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;//创建时间
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;//更新时间
}

NotNull和NotEmpty是有区别的。NotNull是不能不传。NoEmpty如果是字符串的话,还不能是空字符串。
在CategoryService中添加update方法。

1
2
3
4
5
6
7
8
9
10
//在categoryService接口中
//更新分类
void update(Category category);

//在categoryService实现类中
@Override
public void update(Category category) {
category.setUpdateTime(LocalDateTime.now());
categoryMapper.update(category);
}

在CategoryMapper中添加对应的sql语句。

1
2
3
//更新
@Update("update category set category_name=#{categoryName},category_alias=#{categoryAlias},update_time=#{updateTime} where id=#{id}")
void update(Category category);

29 实战篇-更新文章分类和添加文章分类_分组校验

把校验项进行分组,在完成不同的功能的时候,校验指定组中的校验项。
1 定义分组。
2 定义校验项时指定归属的分组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Data
public class Category {
@NotNull(groups = Update.class)
private Integer id;//主键ID
@NotEmpty(groups = {Add.class, Update.class})
private String categoryName;//分类名称
@NotEmpty(groups = {Add.class, Update.class})
private String categoryAlias;//分类别名
private Integer createUser;//创建人ID
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;//创建时间
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;//更新时间

public interface Add{}
public interface Update{}
}

plus版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Data
public class Category {
@NotNull(groups = Update.class)
private Integer id;//主键ID
@NotEmpty
private String categoryName;//分类名称
@NotEmpty
private String categoryAlias;//分类别名
private Integer createUser;//创建人ID
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;//创建时间
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;//更新时间

//如果说某个校验项没有指定分组,默认属于Default分组。
//分组之间可以继承,A extends B,那么A中拥有B中所有的校验项。
//这样就不需要同时指定两个分组了,相当于A和B都是从Default中继承而来的
public interface Add extends Default {}
public interface Update extends Default {}
}

3 校验时指定要校验的分组。

1
2
3
4
5
6
7
8
9
10
11
@PostMapping
public Result add(@RequestBody @Validated(Category.Add.class) Category category) {
categoryService.add(category);
return Result.success();
}

@PutMapping
public Result update(@RequestBody @Validated(Category.Update.class) Category category) {
categoryService.update(category);
return Result.success();
}

定义校验项时如果没有指定分组,则属于Default分组,分组可以继承。

30 实战篇-更新文章

在ArticleController中添加add方法。

1
2
3
4
5
6
7
8
9
10
11
@RestController
@RequestMapping("/article")
public class ArticleController {
@Autowired
private ArticleService articleService;
@PostMapping
public Result add(@RequestBody Article article){
articleService.add(article);
return Result.success();
}
}

在ArticleService中添加add方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//在ArticleService接口中
//新增文章
void add(Article article);
//在ArticService实现类中
@Service
public class ArticleServiceImpl implements ArticleService {
@Autowired
private ArticleMapper articleMapper;
@Override
public void add(Article article) {
//补充属性值
article.setCreateTime(LocalDateTime.now());
article.setUpdateTime(LocalDateTime.now());
Map<String,Object> map = ThreadLocalUtil.get();
Integer userId = Integer.parseInt(map.get("id").toString());
article.setCreateUser(userId);
articleMapper.add(article);
}
}

在ArticleMapper中添加insert语句。

1
2
3
4
5
6
7
@Mapper
public interface ArticleMapper {
//新增
@Insert("insert into article(title,content,cover_img,state,category_id,create_user,create_time,update_time) " +
"values(#{title},#{content},#{coverImg},#{state},#{categoryId},#{createUser},#{createTime},#{updateTime})")
void add(Article article);
}

31 实战篇-新增文章参数校验_自定义校验

对除了state之外的其他字段添加校验。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//在实体类中
@Data
public class Article {
private Integer id;//主键ID
@NotEmpty //对字符串
@Pattern(regexp = "^//S{1,10}$")
private String title;//文章标题
@NotEmpty
private String content;//文章内容
@NotEmpty
@URL
private String coverImg;//封面图像
private String state;//发布状态 已发布|草稿
@NotNull //对于整数
private Integer categoryId;//文章分类id
private Integer createUser;//创建人ID
private LocalDateTime createTime;//创建时间
private LocalDateTime updateTime;//更新时间
}

//在controller层中
@PostMapping
public Result add(@RequestBody @Validated Article article){
articleService.add(article);
return Result.success();
}

对于state,需要用到自定义校验。

自定义校验是指已有的注解不能满足所有的校验需求,特殊的情况需要自定义校验(自定义校验注解)。

1 自定义注解state。
模仿NotEmpty注解写代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Documented //元注解
@Constraint(//指定谁为注解提供校验规则
validatedBy = {StateValidation.class}//指定提供校验规则的类
)
@Target({ElementType.FIELD})//元注解,标识state注解用在哪些地方,field表示对属性进行校验
@Retention(RetentionPolicy.RUNTIME)//元注解,标识state注解会在哪个阶段被保留,是编译阶段呢还是源码阶段还是运行时阶段,这里我们需要保留到运行时阶段。
public @interface State {
//提供校验失败后的提示信息
String message() default "state参数的值只能是已发布或者草稿";
//指定分组
Class<?>[] groups() default {};
//负载 获取到State注解的附加信息
Class<? extends Payload>[] payload() default {};
}

2 自定义校验校验数据的类StateValidation,实现ConstraintValidator接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//public class StateValidation implements ConstraintValidator<给哪个注解提供校验规则,校验的数据类型> {
public class StateValidation implements ConstraintValidator<State,String> {
/**
* @param value 将来要校验的数据
* @param constraintValidatorContext context in which the constraint is evaluated
* @return 如果返回false则校验不通过;如果返回true则校验通过。
*/

@Override
public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
//提供校验规则
if(value == null){
return false;
}
if(value.equals("已发布")||value.equals("草稿")){
return true;
}
return false;
}
}

3 在需要校验的地方使用自定义注解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Data
public class Article {
private Integer id;//主键ID
@NotEmpty //对字符串
@Pattern(regexp = "^\\S{1,10}$")
private String title;//文章标题
@NotEmpty
private String content;//文章内容
@NotEmpty
@URL
private String coverImg;//封面图像
@State
private String state;//发布状态 已发布|草稿
@NotNull //对于整数
private Integer categoryId;//文章分类id
private Integer createUser;//创建人ID
private LocalDateTime createTime;//创建时间
private LocalDateTime updateTime;//更新时间
}

32 实战篇-文章分类列表查询(条件分类)

在ArticleController中新增list方法。需要返回total和一个list包含多个文章。

1
2
3
4
5
6
7
8
9
10
11
//PageBean需要一个泛型用来描述单个元素是什么
@GetMapping
public Result<PageBean<Article>> list(
Integer pageNum, //必须和接口文档里的请求参数一致
Integer pageSize,
@RequestParam(required = false) Integer categoryId,
@RequestParam(required = false) String state
){
PageBean<Article> pb = articleService.list(pageNum,pageSize,categoryId,state);
return Result.success(pb);
}

在ArticleService中新增list方法。构建一个PageBean对象用来封装要查询的数据。分页查询使用PageHelper(mybatis插件)来实现。
导入pagehelper依赖。

1
2
3
4
5
6
<!--pageHelper坐标-->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.7</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//在ArticleService接口中
//条件分页列表查询
PageBean<Article> list(Integer pageNum, Integer pageSize, Integer categoryId, String state);

//在ArticleService实现类中
@Override
public PageBean<Article> list(Integer pageNum, Integer pageSize, Integer categoryId, String state) {
//1 创建pageBean对象
PageBean<Article> pb = new PageBean<>();
//2 开启分页查询 pageHelper
PageHelper.startPage(pageNum,pageSize);
//3 调用mapper 不需要传pageNum和pageSize,因为有了PageHelper之后,他会自动把pageNum和pageSize拼接到sql语句后面
Map<String,Object> map = ThreadLocalUtil.get();
Integer userId = Integer.parseInt(map.get("id").toString());
List<Article> as = articleMapper.list(userId,categoryId,state);
//把as强转成PageHelper提供的list对象
// page中提供了方法,可以获取PageHelper分页查询后得到的总记录条数和当前页数据
Page<Article> p = (Page<Article>) as;
//把数据填充到PageBean对象中
pb.setTotal(p.getTotal());
pb.setItems(p.getResult());
return pb;
}

在ArticleMapper中新增相应的Mapper映射条件。
映射配置文件ArticleService.xml所在的路径必须和Mapper保持一致,该ArticleService.xml文件的名字必须和ArticleMapper.java的名字保持一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?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.kongming.mapper.ArticleMapper">
<!--动态sql,where标签的作用是如果有条件就加这个关键字,如果没有条件,就不加这个关键字,and会自己去掉-->
<select id="list" resultType="com.kongming.pojo.Article">
select * from article
<where>
<if test="categoryId!=null">
<!--表中的字段名-Mapper接口中的变量名-->
category_id=#{categoryId}
</if>
<if test="state!=null">
and state=#{state}
</if>

and create_user=#{userId}
</where>
</select>

</mapper>
1
2
//使用映射配置文件来写sql,而不用注解
List<Article> list(Integer userId, Integer categoryId, String state);

pageNum表示页数,pageSize表示每页的article数量,pageNum=1表示查询的是第一页的数据,如果此时pageSize=2,则表示第一页里需要两条article数据。
controller中函数的参数名字必须和接口文档中的一致。

33 实战篇-文件上传_本地存储

前端页面三要素。
1 请求方式必须是post。
2 enctype必须是multipart/form-data。
3 文件表单下对应的tpye类型必须是file。

后端MultipartFile。

1
2
3
4
5
String getOriginalFilename();//获得原始文件的名字,也就是用户本地存放的文件的名字
void transferTo(File dest);//将接收的文件转存到磁盘文件中
long getSize();//获取文件的大小,单位:字节
byte[] getBytes();//获取文件内容的字节数组
InputStream getInputStream();//获取接收到的文件内容的输入流

在FileUploadController中新增upload方法。获取文件内容的输入流,写入到本地磁盘文件。

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
public class FileUploadController {
@PostMapping("/upload")
public Result<String> upload(MultipartFile file) throws IOException {
//把文件的内容存储到本地磁盘上
String originalFilename = file.getOriginalFilename();
//保证文件的名字是唯一的,从而防止文件覆盖
String filename = UUID.randomUUID().toString()+originalFilename.substring(originalFilename.lastIndexOf("."));
file.transferTo(new File("C:\\SunYuQi\\data\\研一\\工作\\files\\"+filename));
return Result.success("url访问地址...");
}
}

34 实战篇-文件上传_阿里云OSS_准备工作

云:互联网上的一堆计算机。

阿里云:阿里云是阿里巴巴集团旗下全球领先的云计算公司,也是国内最大的云服务提供商。

阿里云OSS:阿里云对象存储OSS(Object Storage Service),是一款海量,安全,低成本,高可靠的云存储服务。使用OSS,可以通过网络随时存储和调用包括文本,图片,音频和视频等在内的各种文件。

第三方服务-通用思路

准备工作->参照官方SDK编写入门程序->继承使用

SDK:Software Development Kit的缩写,软件开发工具包,包括辅助软件开发的依赖(jar包),代码示例等,都可以叫做SDK。

准备工作

注册登录(实名认证)->充值->开通对象存储服务(OSS)->创建bucket->获取AccessKey(密钥)->参照官方SDK编写入门程序->案例集成OSS

bucket:存储空间是用户用于存储对象(Object,就是文件)的容器,所有的对象都必须隶属于某个存储空间。

35 实战篇-文件上传_阿里云OSS_入门程序

下载SDK。在pom.xml中引入阿里云提供的依赖。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!--阿里云oss依赖坐标-->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.17.4</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>
<!-- no more than 2.3.3-->
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>2.3.3</version>
</dependency>

文件上传demo代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public class Demo {

public static void main(String[] args) throws Exception {
// Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
String endpoint = "https://oss-cn-beijing.aliyuncs.com";
// 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
// EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();
String accessKeyId = "";
String accessKeySecret = "";
// 填写Bucket名称,例如examplebucket。
String bucketName = "kongming234";
// 填写Object完整路径,完整路径中不能包含Bucket名称,例如exampledir/exampleobject.txt。
String objectName = "001.jpg";
// 填写Bucket所在地域。以华东1(杭州)为例,Region填写为cn-hangzhou。
String region = "cn-beijing";

// 创建OSSClient实例。
// ClientBuilderConfiguration clientBuilderConfiguration = new ClientBuilderConfiguration();
// clientBuilderConfiguration.setSignatureVersion(SignVersion.V4);
// OSS ossClient = OSSClientBuilder.create()
// .endpoint(endpoint)
// .credentialsProvider(credentialsProvider)
// .clientConfiguration(clientBuilderConfiguration)
// .region(region)
// .build();
//尝试用可以直接使用accesskey的代码
OSS ossClient = new OSSClient(endpoint, accessKeyId, accessKeySecret);

try {
// 填写字符串。
String content = "Hello OSS,你好世界";

// 创建PutObjectRequest对象。把content内容转换为一个输入流对象。那我们可以直接把图片的输入流放到这个位置
PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, objectName, new FileInputStream("C:\\SunYuQi\\data\\研一\\工作\\files\\孔明.jpg"));

// 如果需要上传时设置存储类型和访问权限,请参考以下示例代码。
// ObjectMetadata metadata = new ObjectMetadata();
// metadata.setHeader(OSSHeaders.OSS_STORAGE_CLASS, StorageClass.Standard.toString());
// metadata.setObjectAcl(CannedAccessControlList.Private);
// putObjectRequest.setMetadata(metadata);

// 上传字符串。
PutObjectResult result = ossClient.putObject(putObjectRequest);
} catch (OSSException oe) {
System.out.println("Caught an OSSException, which means your request made it to OSS, "
+ "but was rejected with an error response for some reason.");
System.out.println("Error Message:" + oe.getErrorMessage());
System.out.println("Error Code:" + oe.getErrorCode());
System.out.println("Request ID:" + oe.getRequestId());
System.out.println("Host ID:" + oe.getHostId());
} catch (ClientException ce) {
System.out.println("Caught an ClientException, which means the client encountered "
+ "a serious internal problem while trying to communicate with OSS, "
+ "such as not being able to access the network.");
System.out.println("Error Message:" + ce.getMessage());
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
}
}

36 实战篇-上传文件_阿里云OSS_程序集成

阿里云工具类开发,实际上就是对上面demo的修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public class AliOssUtil {
// Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
private static final String ENDPOINT = "https://oss-cn-beijing.aliyuncs.com";
// 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
// EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();
private static final String ACCESS_KEY_ID = "";
private static final String ACCESS_KEY_SECRET = "";
// 填写Bucket名称,例如examplebucket。
private static final String BUCKET_NAME = "kongming234";
public static String uploadFile(String objectName, InputStream in) throws Exception {

// 创建OSSClient实例。
// ClientBuilderConfiguration clientBuilderConfiguration = new ClientBuilderConfiguration();
// clientBuilderConfiguration.setSignatureVersion(SignVersion.V4);
// OSS ossClient = OSSClientBuilder.create()
// .endpoint(endpoint)
// .credentialsProvider(credentialsProvider)
// .clientConfiguration(clientBuilderConfiguration)
// .region(region)
// .build();
//尝试用可以直接使用accesskey的代码
OSS ossClient = new OSSClient(ENDPOINT, ACCESS_KEY_ID, ACCESS_KEY_SECRET);
String url = "";
try {
// 填写字符串。
String content = "Hello OSS,你好世界";

// 创建PutObjectRequest对象。把content内容转换为一个输入流对象。那我们可以直接把图片的输入流放到这个位置
PutObjectRequest putObjectRequest = new PutObjectRequest(BUCKET_NAME, objectName, in);

// 如果需要上传时设置存储类型和访问权限,请参考以下示例代码。
// ObjectMetadata metadata = new ObjectMetadata();
// metadata.setHeader(OSSHeaders.OSS_STORAGE_CLASS, StorageClass.Standard.toString());
// metadata.setObjectAcl(CannedAccessControlList.Private);
// putObjectRequest.setMetadata(metadata);

// 上传字符串。
PutObjectResult result = ossClient.putObject(putObjectRequest);
//url组成:https://bucket名称.区域节点/objectName
url = "https://"+BUCKET_NAME+"."+ENDPOINT.substring(ENDPOINT.lastIndexOf("/")+1)+"/"+objectName;
} catch (OSSException oe) {
System.out.println("Caught an OSSException, which means your request made it to OSS, "
+ "but was rejected with an error response for some reason.");
System.out.println("Error Message:" + oe.getErrorMessage());
System.out.println("Error Code:" + oe.getErrorCode());
System.out.println("Request ID:" + oe.getRequestId());
System.out.println("Host ID:" + oe.getHostId());
} catch (ClientException ce) {
System.out.println("Caught an ClientException, which means the client encountered "
+ "a serious internal problem while trying to communicate with OSS, "
+ "such as not being able to access the network.");
System.out.println("Error Message:" + ce.getMessage());
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
return url;
}
}

在controller中修改。

1
2
3
4
5
6
7
8
9
10
@PostMapping("/upload")
public Result<String> upload(MultipartFile file) throws Exception {
//把文件的内容存储到本地磁盘上
String originalFilename = file.getOriginalFilename();
//保证文件的名字是唯一的,从而防止文件覆盖
String filename = UUID.randomUUID().toString()+originalFilename.substring(originalFilename.lastIndexOf("."));
//file.transferTo(new File("C:\\SunYuQi\\data\\研一\\工作\\files\\"+filename));
String url = AliOssUtil.uploadFile(filename,file.getInputStream());
return Result.success(url);
}

37 实战篇-登录优化-redis_思路分析

登录:在用户修改了密码之后,服务器会下发新令牌,旧令牌应该作废。但之前的程序中并没有作废旧的令牌。

令牌主动失效机制:(虎符)

  • 登录成功后,给浏览器响应令牌的同时,把该令牌存储到redis中。
  • LoginInterceptor拦截器中,需要验证浏览器携带的令牌,并同时需要获取到redis中存储的与之相同的令牌。
  • 当用户修改密码成功后,删除redis中存储的旧令牌。

38 实战篇-登录优化_redis_SpringBoot集成redis

SpringBoot集成redis

  • 导入spring-boot-starter-data-redis起步依赖

在redis起步依赖中,会自动往ROC容器中注入StringRedisTemplate对象。

1
2
3
4
5
<!--redis坐标-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  • 在yml配置文件中,配置redis连接信息
1
2
3
4
5
6
7
8
9
10
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/kongming
username: root
password: 181234
data:
redis:
host: localhost
port: 6379
  • 调用API(StringRedisTemplate)完成字符串的存取操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@SpringBootTest//如果在测试类上添加了这个注解,那么将来单元测试方法执行之前,会先初始化Spring容器
public class RedisTest {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Test
public void testSet(){
//往redis中存储一个键值对
//调用StringRedisTemplate
ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
operations.set("username","kongming");
operations.set("id","1",15, TimeUnit.SECONDS);
}
@Test
public void testGet(){
//从redis中获取一个键值对
ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
System.out.println(operations.get("username"));
}
}

39实战篇-登录优化_redis_主动失效机制实现

令牌主动失效机制:(虎符)

  • 登录成功后,给浏览器响应令牌的同时,把该令牌存储到redis中。
    在userContorller中修改。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@PostMapping("/login")
public Result<String> login(@Pattern(regexp = "^\\S{5,16}$") String username, @Pattern(regexp = "^\\S{5,16}$") String password) {
//根据用户名查询用户
User loginUser = userService.findByUserName(username);
//判断该用户是否存在
if(loginUser == null){
return Result.error("用户名错误");
}
//判断密码是否正确。loginUser对象中的
if(Md5Util.getMD5String(password).equals(loginUser.getPassword())){
//登录成功
Map<String, Object> claims = new HashMap<>();
claims.put("id",loginUser.getId());
claims.put("username",loginUser.getUsername());
String token = JwtUtil.genToken(claims);
//把token存储到redis中
ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
operations.set(token,token,1, TimeUnit.HOURS);
return Result.success(token);
}
return Result.error("密码错误");
}
  • LoginInterceptor拦截器中,需要验证浏览器携带的令牌,并同时需要获取到redis中存储的与之相同的令牌。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//令牌验证
String token = request.getHeader("Authorization");
//在提供服务之前验证token
//CTRL+Alt+T 直接try catch
try {
//从redis中获取相同的token
ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
String redisToken = operations.get(token);
if (redisToken == null) {
//token已经失效了
throw new RuntimeException();
}
Map<String, Object> claims = JwtUtil.parseToken(token);
//把业务数据存储到ThreadLocal中
ThreadLocalUtil.set(claims);
//放行
return true;
} catch (Exception e) {
//http响应状态码为401
response.setStatus(401);
//不放行
return false;
}
}
  • 当用户修改密码成功后,删除redis中存储的旧令牌。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// @RequestBody实现请求体里的json格式的数据转换为map对象
@PatchMapping("/updatePwd")
public Result updatePwd(@RequestBody Map<String,String> params,@RequestHeader("Authorization") String token){
//1 校验参数
//validation提供的注解无法满足参数校验的需求
String oldPwd = params.get("old_pwd");
String newPwd = params.get("new_pwd");
String rePwd = params.get("re_pwd");

if(!StringUtils.hasLength(oldPwd)||!StringUtils.hasLength(newPwd)||!StringUtils.hasLength(rePwd)){
return Result.error("缺少必要的参数");
}
//原密码是否正确
//调用userService根据用户名拿到原密码,再和old_pwd比对
Map<String,Object> map = ThreadLocalUtil.get();
String username = map.get("username").toString();
User loginUser = userService.findByUserName(username);
//获取的是加密后的密码
if(!loginUser.getPassword().equals(Md5Util.getMD5String(oldPwd))){
return Result.error("原密码填写不正确");
}

//newPwd和rePwd填写是否一样
if(!rePwd.equals(newPwd)){
return Result.error("两次填写的新密码不一样");
}
//2 调用service实现密码更新
userService.updatePwd(newPwd);
//3 删除redis中对应的token
ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
operations.getOperations().delete(token);

return Result.success();
}

40实战篇-SpringBoot项目部署

SpringBoot项目部署时,SpringBoot需要一个打包插件spring-boot-maven-plugin。

1
2
3
4
5
6
7
8
9
10
<build>
<plugins>
<!--打包插件-->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>3.3.1</version>
</plugin>
</plugins>
</build>

1 找到IDEA右侧的maven菜单,big-event->Lifecycle->package(双击开始打包)。

2 运行jar包。

1
java -jar big-event-1.0-SNAPSHOT.jar

3 注意:jar包部署,要求服务器必须有jre环境。

41实战篇-SpringBoot属性配置方式

希望部署好的端口是9090不是8080。
项目配置文件方式:

  • application.properties配置文件
  • application.yml配置文件(常用,键值对)

命令行参数方式:

1
2
java -jar big-event-1.0-SNAPSHOT.jar --键=值
java -jar big-event-1.0-SNAPSHOT.jar --server.port=10010

该参数会传递给启动类的main方法,用一个args数组来接收。

环境变量方式:
在administrator的用户环境变量中添加server.port,值为8888。
环境变量发生变化,cmd必须重新启动一下。

外部配置文件方式:
在jar包所在的路径下新建一个application.yml文件。

1
2
server:
port: 9999

注意port:后面有一个空格。

配置优先级:

  • 项目中resources目录下的application.yml.
  • jar包所在目录下的application.yml
  • 操作系统环境变量
  • 命令行参数

他们的优先级依次升高。

42实战篇-SpringBoot多环境开发-基本使用

开发环境,测试环境,生产环境

多环境开发的单文件使用

Pofiles:SpringBoot提供的Profiles可以用来隔离应用程序配置的各个部分,并在特定环境下指定部分配置生效。
如何分隔不同环境的配置:

1
---

如何指定哪些配置属于哪个环境?

1
2
3
4
spring:
config:
activate:
on-profile:环境名称

如何指定哪个环境的配置生效?

1
2
3
spring:
profiles:
activate: 环境名称
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/kongming
username: root
password: 181234
data:
redis:
host: localhost
port: 6379
profiles:
active: test

mybatis:
configuration:
map-underscore-to-camel-case: true # 开启驼峰命名和下划线命名的自动转换
# 通用信息,指定生效的环境
# 多环境下的共性属性
# 如果特定环境中的配置和通用信息冲突了,特定环境中的配置生效。

---
# 开发环境
spring:
config:
activate:
on-profile: dev
server:
port: 8081
---
# 测试环境
spring:
config:
activate:
on-profile: test

server:
port: 8082
---
# 生产环境

spring:
config:
activate:
on-profile: pro

server:
port: 8083

多环境开发的多文件使用

SpringBoot提供的Profiles可以用来隔离应用程序配置的各个部分,并在特定环境下指定某些部分的配置生效。
开发 application-dev.yml
测试 application-test.yml
生产 application-pro.yml
共性配置并激活指定环境 application.yml。在该文件中使用dev,test,pro来激活相应配置文件。

43实战篇-SpringBoot多环境开发-分组

多环境开发Profiles分组功能:

  • 服务器相关配置 application-devServer.yml
  • 数据源相关配置 application-devDB.yml
  • 自定义配置 application-devSelf.yml
    在application.yaml中定义分组和激活指定分组。
1
2
3
4
5
6
spring:
profiles:
active: dev
group:
"dev": devServer,devDB,devSelf
"test": testServier,testDB,testSelf

44实战篇-大事件前端项目开发_前置知识_js导入导出

前置知识

HTML,CSS,JS
HTML:负责网页的结构。(标签:form/table/a/div/span)
CSS:负责网页的表现。(样式:color/font/background/width/height)
JavaScript:负责网页的行为。(交互效果)

JavaScript导入导出-按需导入

JS提供的导入导出机制,可以实现按需导入。

1
2
3
4
5
6
7
8
9
//简单的展示信息
//使用export关键字实现导出
export function simpleMessage(msg){
console.log(msg)
}
//复杂的展示信息
export function complexMessage(msg){
console.log(new Data()+": "+msg)
}
1
2
3
4
5
6
7
<!--导入showMessage.js文件的全部内容-->
<script src="showMessage.js"></script>

<!--部分导入-->
<script type="module">
import {complexMessage} from './showMessage.js'
<\script>

JavaScript导入导出-批量导出

js还可以实现批量导出。

1
2
3
4
5
6
7
8
9
//简单的展示信息
function simpleMessage(msg){
console.log(msg)
}
//复杂的展示信息
function complexMessage(msg){
console.log(new Data()+": "+msg)
}
export {simpleMessage,complexMessage}

JavaScript导入导出-重命名

为js中函数实现重命名。

1
2
3
4
5
6
7
8
9
10
11
12
<body>
<div id="app">
<button id="btn">点我展示信息</button>
</div>
<script type="module">
import {complexMessage as cm} from './showMessage.js';
document.getElementById("btn").onclick=function(){
cm('我被点击了...')
}
</script>

</body>

还可以在批量导出时定义别名。

1
2
//批量导出
export {complexMessage as cm,simpleMessage as sm}

在html导入时要相应的修改名字。不可以使用原来的名字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<body>
<div id="app">
<button id="btn">点我展示信息</button>
</div>
<script type="module">
import {cm} from './showMessage.js';
document.getElementById("btn").onclick=function(){
cm('我被点击了...')
}
</script>

</body>

</html>

JavaScript默认导出

1
2
3
4
5
6
7
8
9
//简单的展示信息
function simpleMessage(msg){
console.log(msg)
}
//复杂的展示信息
function complexMessage(msg){
console.log(new Data()+": "+msg)
}
export default {simpleMessage,complexMessage}

相应的导入代码为

1
2
3
4
5
6
7
8
9
10
11
12
13
<body>
<div id="app">
<button id="btn">点我展示信息</button>
</div>
<script type="module">
//messageMethods代表所有从./showMessage.js文件导出的函数
import messageMethods+ from './showMessage.js';
messageMethods.simpleMessage('aaa');
</script>

</body>

</html>

45实战篇-vue概述

Vue是一款用于构建用户界面的渐进式的JavaScript框架。(官方)
渐进式的含义是既可以局部改造,又可以整站开发。

学习路径:
局部使用vue:

  • 快速入门
  • 常用指令
  • 生命周期

整站使用vue:

  • Vue项目构建工具
  • Vue项目目录结构
  • Vue项目开发流程
  • Element-Plus

大事件前端:

  • 客户端路由
  • 状态管理

46实战篇-vue快速入门

47实战篇-vue指令_v-for

48实战篇-vue指令_v-bind

49实战篇-vue指令_v-if和v-show