SpringBoot3(二)

主要是黑马的苍穹外卖项目学习记录

一、Springboot3依赖及其用法

1、lombok

1
2
3
4
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
  1. 方便打日志

    类上加@Slf4j

    1
    log.info("新增员工:{}"+employeeDTO);
  2. @Data

    给属性自动加上getter以及setter方法

  3. @Builder

    1
    2
    3
    Employee employee = Employee.builder()
    .id(id)
    .status(status).build();

二、其他用法

1、加密密码

通过MD5加密密码

1
password = DigestUtils.md5DigestAsHex(password.getBytes());

2、后端统一返回结果格式

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
/**
* 后端统一返回结果
* @param <T>
*/
@Data
public class Result<T> implements Serializable {

private Integer code; //编码:1成功,0和其它数字为失败
private String msg; //错误信息
private T data; //数据

public static <T> Result<T> success() {
Result<T> result = new Result<T>();
result.code = 1;
return result;
}

public static <T> Result<T> success(T object) {
Result<T> result = new Result<T>();
result.data = object;
result.code = 1;
return result;
}

public static <T> Result<T> error(String msg) {
Result result = new Result();
result.msg = msg;
result.code = 0;
return result;
}

}

3、JWT工具类(另)

1
2
3
4
5
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.11.5</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
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
public class JwtUtil {
/**
* 生成jwt
* 使用Hs256算法, 私匙使用固定秘钥
*
* @param secretKey jwt秘钥
* @param ttlMillis jwt过期时间(毫秒)
* @param claims 设置的信息
* @return
*/
public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) {
// 指定签名的时候使用的签名算法,也就是header那部分
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

// 生成JWT的时间
long expMillis = System.currentTimeMillis() + ttlMillis;
Date exp = new Date(expMillis);

// 设置jwt的body
JwtBuilder builder = Jwts.builder()
// 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
.setClaims(claims)
// 设置签名使用的签名算法和签名使用的秘钥
.signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))
// 设置过期时间
.setExpiration(exp);

return builder.compact();
}

/**
* Token解密
*
* @param secretKey jwt秘钥 此秘钥一定要保留好在服务端, 不能暴露出去, 否则sign就可以被伪造, 如果对接多个客户端建议改造成多个
* @param token 加密后的token
* @return
*/
public static Claims parseJWT(String secretKey, String token) {
// 得到DefaultJwtParser
Claims claims = Jwts.parser()
// 设置签名的秘钥
.setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))
// 设置需要解析的jwt
.parseClaimsJws(token).getBody();
return claims;
}

}

4、数据库操作时间字段显示有问题

  1. 方式一

    @JsonFormat(pattern = “yyyy-MM-dd HH:mm:ss”)

在属性上加上注解 ,对日期进行格式化

  1. 序列化:当 Event 对象被转化为 JSON 时,eventDate 字段会以指定的格式 yyyy-MM-dd HH:mm:ss 进行输出。

  2. 反序列化:当一个 JSON 字符串被转化为 Event 对象时,eventDate 字段会根据给定的格式 "yyyy-MM-dd HH:mm:ss" 进行解析。

  3. 方式二

消息转换器用于将 HTTP 请求或响应的内容从一个格式(如 JSON、XML)转换为 Java 对象,或者反向操作,从 Java 对象转换为相应的格式(如 JSON、XML)。

在WebMvcConfiguration中扩展SpringMVC的消息转换器,统一对日期类型进行格式处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 配置类,注册web层相关组件
*/
@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {

/**
* 扩展spring mvc框架的消息转换器
* @param converters
*/
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("扩展消息转换器...");
//创建一个消息转换器对象
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
//需要为消息转换器设置一个对象转换器, 对象转换器可以将java对象序列化为json数据
converter.setObjectMapper(new JacksonObjectMapper());


//将自己的消息转换器加入到容器中
converters.add(0,converter);
}
}
1
2
3
4
5
6
7
8
9
10
package com.sky.json;

public class JacksonObjectMapper extends ObjectMapper {

//.......
public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm";
//.......

}
}

5、自定义注解@AutoFill

其中一个例子:在创建、更新对象的时候,自动填充对象的创建时间更新时间等内容

实现步骤:

1). 自定义注解 AutoFill,用于标识需要进行公共字段自动填充的方法

2). 自定义切面类 AutoFillAspect,统一拦截加入了 AutoFill 注解的方法,通过反射为公共字段赋值

3). 在 Mapper 的方法上加入 AutoFill 注解

  1. 自定义注解

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    /**
    * 自定义注解,用于标识某个方法需要进行功能字段自动填充处理
    */
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface AutoFill {
    //指定当前数据库操作类型 update insert
    OperationType value();

    }

    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
    	/**
    * 数据库操作类型
    */
    public enum OperationType {

    /**
    * 更新操作
    */
    UPDATE,

    /**
    * 插入操作
    */
    INSERT
    }

    2. 自定义切面

    主要结构:

    ``` java
    /**
    * 自定义切面,实现公共字段自动填充处理逻辑
    */
    @Aspect
    @Component
    @Slf4j
    public class AutoFillAspect {

    /**
    * 切入点,对所有加@AutoFill注解同时在com.sky.mapper包下的方法进行切入
    */
    @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
    public void autoFillPointCut(){}

    /**
    * 前置通知,在通知中进行公共字段的赋值,Before中写切点
    */
    @Before("autoFillPointCut()")
    public void autoFill(JoinPoint joinPoint){
    /////////////////////重要////////////////////////////////////
    //可先进行调试,是否能进入该方法 提前在mapper方法添加AutoFill注解
    log.info("开始进行公共字段自动填充...");

    }
    }

    具体方法如下(仅供参考):

    复杂用法 解释
    JoinPoint 连接点的信息
    joinPoint.getSignature() 得到被拦截方法的签名(即方法的名字、返回类型、参数类型等)
    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
    @Before("autoFillPointCut()")
    public void autoFill(JoinPoint joinPoint) throws InvocationTargetException, IllegalAccessException {
    log.info("开始进行公共字段的自动填充...");

    //获取到当前被拦截的方法上的数据库操作类型
    MethodSignature signature = (MethodSignature) joinPoint.getSignature();
    AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class); // 获得方法上的注解对象
    OperationType operationType = autoFill.value(); //得到注解对象的值


    //获取到当前拦截方法的参数--实体对象
    Object[]args = joinPoint.getArgs();
    if(args == null || args.length== 0){
    return;
    }

    Object entity = args[0];
    //准备复制的数据
    LocalDateTime now = LocalDateTime.now();
    Long currentId = BaseContext.getCurrentId();

    //当前不同的操作类型对应的属性赋值,通过反射
    if(operationType == OperationType.INSERT){
    //为四个公共字段赋值
    try {
    Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
    Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
    Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
    Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);

    //通过反射为对象属性赋值
    setCreateTime.invoke(entity,now);
    setCreateUser.invoke(entity,currentId);
    setUpdateTime.invoke(entity,now);
    setUpdateUser.invoke(entity,currentId);
    } catch (NoSuchMethodException e) {
    throw new RuntimeException(e);
    }
    }else if(operationType == OperationType.UPDATE){
    //为2个公共字段赋值

    try {
    Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
    Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);

    //通过反射为对象属性赋值
    setUpdateTime.invoke(entity,now);
    setUpdateUser.invoke(entity,currentId);
    } catch (NoSuchMethodException e) {
    throw new RuntimeException(e);
    }
    }
    }

    反射机制:

    1. Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class)

      得到参数实体名为AutoFillConstant.SET_CREATE_TIME,方法参数为 LocalDateTime.class的方法

    2. setCreateTime.invoke(entity,now);

​ 动态执行entity对应的setCreateTime方法传入now(当前时间)作为参数

  1. 对应地方使用

    1
    2
    @AutoFill(OperationType.UPDATE)
    void update(Employee employee);

三、Swagger

Swagger 是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务。快速生成接口文档,并且可以测试接口

Knife4j 主要特点包括:

  • Swagger UI 增强:Knife4j 提供了一个更美观、更易用的 Swagger UI 界面,增加了更多的定制功能。

  • 多 API 文档支持:Knife4j 支持多个 Swagger 文档的整合显示,可以展示多个 API 的文档。

  • 增强的代码生成:通过 Knife4j,你可以对接口文档进行更多的配置和自定义,提升 API 文档的交互体验。

1、依赖

1
2
3
4
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
</dependency>

2、配置文件

在配置类中加上Knife4j相关配置,一般是webMVC配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Bean
public Docket docket() {
//配置文档相关信息
ApiInfo apiInfo = new ApiInfoBuilder()
.title("苍穹外卖项目接口文档")
.version("2.0")
.description("苍穹外卖项目接口文档")
.build();
Docket docket = new Docket(DocumentationType.SWAGGER_2)
.groupName("管理端接口")
.apiInfo(apiInfo)
.select()
.apis(RequestHandlerSelectors.basePackage("com.sky.controller.admin")) //生成的controller接口位置
.paths(PathSelectors.any())
.build();
return docket;
}

3、设置静态资源映射,否则接口文档页面无法访问

将doc.html自动映射到/META-INF/resources/下的内容

1
2
3
4
5
6
7
8
9
/**
* 设置静态资源映射
* @param registry
*/
protected void addResourceHandlers(ResourceHandlerRegistry registry) {

registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}

使用方式例如:

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
@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {
/**
* 通过knife4j生成接口文档
* @return
*/
@Bean
public Docket docket1() {
ApiInfo apiInfo = new ApiInfoBuilder()
.title("苍穹外卖项目接口文档")
.version("2.0")
.description("苍穹外卖项目接口文档")
.build();
Docket docket = new Docket(DocumentationType.SWAGGER_2)
.groupName("管理端接口")
.apiInfo(apiInfo)
.select()
.apis(RequestHandlerSelectors.basePackage("com.sky.controller.admin"))
.paths(PathSelectors.any())
.build();
return docket;
}
/**
* 设置静态资源映射
* @param registry
*/
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
log.info("开始设置静态资源映射");
registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}
}

4、常用注解

通过注解可以控制生成的接口文档,使接口文档拥有更好的可读性,常用注解如下:

注解 说明
@Api 用在类上,例如Controller,表示对类的说明
@ApiModel 用在类上,例如entity、DTO、VO
@ApiModelProperty 用在属性上,描述属性信息
@ApiOperation 用在方法上,例如Controller的方法,说明方法的用途、作用

例如:

1
2
3
4
5
6
7
8
9
10
11
@Data
@ApiModel(description = "员工登录时传递的数据模型")
public class EmployeeLoginDTO implements Serializable {

@ApiModelProperty("用户名")
private String username;

@ApiModelProperty("密码")
private String password;

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(description = "员工登录返回的数据格式")
public class EmployeeLoginVO implements Serializable {

@ApiModelProperty("主键值")
private Long id;

@ApiModelProperty("用户名")
private String userName;

@ApiModelProperty("姓名")
private String name;

@ApiModelProperty("jwt令牌")
private String token;

}
1
2
3
4
5
6
7
8
9
10
11
12
@RestController
@RequestMapping("/admin/employee")
@Slf4j
@Api(tags = "员工相关接口")
public class EmployeeController {
@PostMapping("/logout")
@ApiOperation("员工退出")
public Result<String> logout() {
return Result.success();
}

}

5、原生Swagger

1
2
3
4
5
<!-- Spring Boot 集成 swagger -->
<dependency>
<groupId>com.spring4all</groupId>
<artifactId>swagger-spring-boot-starter</artifactId>
</dependency>

application.yaml或者bootstrap.yaml

1
2
3
4
5
6
swagger:
title: "学成在线内容管理系统"
description: "内容系统管理系统对课程相关信息进行管理"
base-package: com.xuecheng.content
enabled: true
version: 1.0.0

常用Swagger注解如下:

1
2
3
4
5
6
7
8
9
10
11
@Api:修饰整个类,描述Controller的作用
@ApiOperation:描述一个类的一个方法,或者说一个接口
@ApiParam:单个参数描述
@ApiModel:用对象来接收参数
@ApiModelProperty:用对象接收参数时,描述对象的一个字段
@ApiResponse:HTTP响应其中1个描述
@ApiResponses:HTTP响应整体描述
@ApiIgnore:使用该注解忽略这个API
@ApiError :发生错误返回的信息
@ApiImplicitParam:一个请求参数
@ApiImplicitParams:多个请求参数

四、HttpClient

HttpClient可以:发送HTTP请求接收响应数据

主要用到后端。主动发起HTTP请求,接收响应数据

1
2
3
4
5
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>

1、介绍

HttpClient的核心API:

  • HttpClient:Http客户端对象类型,使用该类型对象可发起Http请求。

  • HttpClients:可认为是构建器,可创建HttpClient对象。

  • CloseableHttpClient:实现类,实现了HttpClient接口。

  • HttpGet:Get方式请求类型。

  • HttpPost:Post方式请求类型。

HttpClient发送请求步骤:

  • 创建HttpClient对象

  • 创建Http请求对象

  • 调用HttpClient的execute方法发送请求

2、使用步骤

实现步骤:

  1. 创建HttpClient对象

  2. 创建请求对象

  3. 发送请求,接受响应结果

  4. 解析结果

  5. 关闭资源

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
@Test
public void testGet() throws IOException {
//创建httpClient对象
CloseableHttpClient httpClient = HttpClients.createDefault();

//创建请求对象
HttpGet httpGet = new HttpGet("http://localhost:8080/user/shop/status");

//发送请求,并且接收响应结果
CloseableHttpResponse response = httpClient.execute(httpGet);

//获取服务端返回的状态码
int code = response.getStatusLine().getStatusCode();
System.out.println("服务端放回的状态码是:"+code);

HttpEntity entity = response.getEntity();
String body = EntityUtils.toString(entity);
System.out.println("服务端返回的数据为:"+body);

//关闭资源
response.close();
httpClient.close();
}

/**
* 测试通过httpclient 发送 post方式请求
*/
@Test
public void testPost() throws IOException {
//创建httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
//创建请求对象

HttpPost httpPost = new HttpPost("http://localhost:8080/admin/employee/login");

JSONObject jsonObject = new JSONObject();
jsonObject.put("username","admin");
jsonObject.put("password","123456");

StringEntity entity = new StringEntity(jsonObject.toString());
//指定请求编码方式
entity.setContentEncoding("UTF-8");
//数据格式
entity.setContentType("application/json");
httpPost.setEntity(entity);
//发送请求

CloseableHttpResponse response = httpClient.execute(httpPost);
//解析放回结果
int code = response.getStatusLine().getStatusCode();
System.out.println("响应码为:"+code);

HttpEntity httpEntity = response.getEntity();
System.out.println("相应结果为:"+EntityUtils.toString(httpEntity));

//关闭资源
response.close();
httpClient.close();
}

3、使用场景

比如服务端主动远程调用第三方验证码服务、调用第三方登录服务

五、Spring Cache

调用spring自带缓存机制,配合Redis使用

1、介绍

Spring Cache 是一个框架,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能。

Spring Cache 提供了一层抽象,底层可以切换不同的缓存实现,例如:

  • EHCache

  • Caffeine

  • Redis(常用)

2、依赖

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId> <version>2.7.3</version>
</dependency>

3、常用注解

注解 说明
@EnableCaching 开启缓存注解功能,通常加在启动类上
@Cacheable 在方法执行前先查询缓存中是否有数据,如果有数据,则直接返回缓存数据;如果没有缓存数据,调用方法并将方法返回值放到缓存中
@CachePut 将方法的返回值放到缓存中
@CacheEvict 将一条或多条数据从缓存中删除

在spring boot项目中,使用缓存技术只需在项目中导入相关缓存技术的依赖包,并在启动类上使用**@EnableCaching**开启缓存支持即可。

例如,使用Redis作为缓存技术,只需要导入Spring data Redis的maven坐标即可

4、使用顺序

  1. 首先在启动类上加**@EnableCaching**注解,表示开启缓存注解功能

    1

  2. 在对应添加方法中使用**@CachePut**注解

    作用:在该用户信息保存到数据库的同时,也往缓存中缓存一份数据

    1

**说明:**key的写法如下

key填写格式 解释
#user.id #user指的是方法形参的名称, id指的是user的id属性 , 也就是使用user的id属性作为key
#result.id #result代表方法返回值,该表达式 代表以返回对象的id属性作为key
#p0.id #p0指的是方法中的第一个参数,id指的是第一个参数的id属性,也就是使用第一个参数的id属性作为key
#a0.id #a0指的是方法中的第一个参数,id指的是第一个参数的id属性,也就是使用第一个参数的id属性作为key
#root.args[0].id #root.args[0]指的是方法中的第一个参数,id指的是第一个参数的id属性,也就是使用第一个参数的id属性作为key
  1. 在对应查询方法中使用**@Cacheable**注解

    当执行方法时会先查询缓存中是否有key,有的话直接查到缓存中的值返回

1

  1. 当对应保存、删除方法中加入**@CacheEvict**注解

​ 当执行此方法的时候自动删除缓存中对应的key以及value1

六、Spring Task

1、介绍

Spring Task 是Spring框架提供的任务调度工具,可以按照约定的时间自动执行某个代码逻辑。

**定位:**定时任务框架 **作用:**定时自动执行某段Java代码

应用场景:

  1. 信用卡每月还款提醒

  2. 生日当天发送生日祝福

  3. 外卖订单下单后长时间未支付,对订单状态定时处理

  4. 外卖收货后,商家没有及时点击送达。我们就可以用一个定时任务自动点击送达

2、cron表达式

cron表达式是一个字符串。其可以表示自定义任务被触发的时间

构成规则:

​ 分为6或7个域(空格隔开):秒、分钟、小时、日、月、周、年(可选)

例子:

​ 2022年10月12日上午9点整 对应的cron表达式为:0 0 9 12 10 ? 2022

说明:一般的值不同时设置,其中一个设置,另一个用?表示。

生成复杂的cron表达式我们可以利用cron表达式在线生成器:https://cron.qqe2.com/

具体细则这里不讨论

3、引入依赖

spring-context 但一般不需要自已引入,spring-boot-starter自带

2

4、编写定时任务类

@Scheduled(cron = “cron表达式”)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 自定义定时任务类
*/
@Component
@Slf4j
public class MyTask {

/**
* 定时任务 每隔5秒触发一次
*/
@Scheduled(cron = "0/5 * * * * ?")
public void executeTask(){
log.info("定时任务开始执行:{}",new Date());
}
}

5、启动类开启任务调度

@EnableScheduling

1
2
3
4
5
6
7
8
9
10
11
@EnableCaching//开启缓存注解
@SpringBootApplication
@EnableTransactionManagement //开启注解方式的事务管理
@Slf4j
@EnableScheduling //开启定时任务
public class SkyApplication {
public static void main(String[] args) {
SpringApplication.run(SkyApplication.class, args);
log.info("server started");
}
}

七、WebSocket

1、介绍

WebSocket 是基于 TCP 的一种新的网络协议。它实现了浏览器与服务器全双工通信——浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接, 并进行双向数据传输。

WebSocket缺点:

服务器长期维护长连接需要一定的成本各个浏览器支持程度不一
WebSocket 是长连接,受网络限制比较大,需要处理好重连

**结论:**WebSocket并不能完全取代HTTP,它只适合在特定的场景下使用

使用场景:

  1. 视频弹幕。弹幕一般是实时更新

  2. 网页聊天。我们需要在网页端实时接收和发送聊天信息

  3. 股票、体育实况的数据更新,不需要刷新网页

  4. 外卖用户催单

2、HTML前端代码

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
63
64
65
66
67
68
69
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>WebSocket Demo</title>
</head>
<body>
<input id="text" type="text" />
<button onclick="send()">发送消息</button>
<button onclick="closeWebSocket()">关闭连接</button>
<div id="message">
</div>
</body>
<script type="text/javascript">
var websocket = null;
var clientId = Math.random().toString(36).substr(2);

//判断当前浏览器是否支持WebSocket
if('WebSocket' in window){
//连接WebSocket节点
websocket = new WebSocket("ws://localhost:8080/ws/"+clientId);
}
else{
alert('Not support websocket')
}

//连接发生错误的回调方法
websocket.onerror = function(){
setMessageInnerHTML("error");
};

//连接成功建立的回调方法
websocket.onopen = function(){
setMessageInnerHTML("连接成功");
}

//接收到消息的回调方法
websocket.onmessage = function(event){
setMessageInnerHTML(event.data);
}

//连接关闭的回调方法
websocket.onclose = function(){
setMessageInnerHTML("close");
}

//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function(){
websocket.close();
}

//将消息显示在网页上
function setMessageInnerHTML(innerHTML){
document.getElementById('message').innerHTML += innerHTML + '<br/>';
}

//发送消息
function send(){
var message = document.getElementById('text').value;
websocket.send(message);
}

//关闭连接
function closeWebSocket() {
websocket.close();
}
</script>
</html>

3、引入依赖

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

4、自定义配置类

1
2
3
4
5
6
7
8
9
10
11
/**
* WebSocket配置类,用于注册WebSocket的Bean
*/
@Configuration
public class WebSocketConfiguration {

@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}

5、编写WebSocket服务代码

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
/**
* WebSocket服务
*/
@Component
@ServerEndpoint("/ws/{sid}")
public class WebSocketServer {

//存放会话对象
private static Map<String, Session> sessionMap = new HashMap();

/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("sid") String sid) {
System.out.println("客户端:" + sid + "建立连接");
sessionMap.put(sid, session);
}

/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, @PathParam("sid") String sid) {
System.out.println("收到来自客户端:" + sid + "的信息:" + message);
}

/**
* 连接关闭调用的方法
*
* @param sid
*/
@OnClose
public void onClose(@PathParam("sid") String sid) {
System.out.println("连接断开:" + sid);
sessionMap.remove(sid);
}

/**
* 群发
*
* @param message
*/
public void sendToAllClient(String message) {
Collection<Session> sessions = sessionMap.values();
for (Session session : sessions) {
try {
//服务器向客户端发送消息
session.getBasicRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}

}

6、配合定时任务使用

1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
public class WebSocketTask {
@Autowired
private WebSocketServer webSocketServer;

/**
* 通过WebSocket每隔5秒向客户端发送消息
*/
@Scheduled(cron = "0/5 * * * * ?")
public void sendMessageToClient() {
webSocketServer.sendToAllClient("这是来自服务端的消息:" + DateTimeFormatter.ofPattern("HH:mm:ss").format(LocalDateTime.now()));
}
}

八、 Apache POI

1、介绍

Apache POI 是一个处理Miscrosoft Office各种文件格式的开源项目。简单来说就是,我们可以使用 POI 在 Java 程序中对Miscrosoft Office各种文件进行读写操作。一般情况下,POI 都是用于操作 Excel 文件

2、依赖

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>3.16</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>3.16</version>
</dependency>

3、写入Excel文件

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
public static void write() throws IOException {
//在内存中创建excel文件
XSSFWorkbook excel = new XSSFWorkbook();
//在excel文件中创建sheet页
XSSFSheet sheet = excel.createSheet("info");
//在sheet页中创建行对象,rownumber编号从0开始
XSSFRow row = sheet.createRow(1);

//创建单元格并且写入文件内容
row.createCell(1).setCellValue("姓名");
row.createCell(2).setCellValue("城市");

//创建一个新行
row = sheet.createRow(2);
row.createCell(1).setCellValue("张三");
row.createCell(2).setCellValue("北京");

row = sheet.createRow(3);
row.createCell(1).setCellValue("李四");
row.createCell(2).setCellValue("南京");

//通过输出流将内存中的excel文件写入到磁盘
FileOutputStream out = new FileOutputStream("D:\\info.xlsx");
excel.write(out);

//关闭资源
out.close();
excel.close();
}

3

4、读取Excel文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static void read() throws IOException {
FileInputStream inputStream = new FileInputStream(new File("D:\\info.xlsx"));

//读取磁盘上已经存在的Excel文件
XSSFWorkbook excel = new XSSFWorkbook(inputStream);
//读取Excel中第一个sheet页
XSSFSheet sheet = excel.getSheetAt(0);
//获取sheet中最后一行的行号
int lastRowNum = sheet.getLastRowNum();

for(int i=1;i<=lastRowNum;i++){
//获得某一行
XSSFRow row = sheet.getRow(i);
//获得单元格对象
String cellValue1 = row.getCell(1).getStringCellValue();
String cellValue2 = row.getCell(2).getStringCellValue();
System.out.println(cellValue1+" "+cellValue2);

//关闭资源
excel.close();
inputStream.close();
}
}

5、如何向浏览器发送文件(下载)

例子:

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
/**导出近30天的运营数据报表
* @param response
**/
public void exportBusinessData(HttpServletResponse response) {
LocalDate begin = LocalDate.now().minusDays(30);
LocalDate end = LocalDate.now().minusDays(1);
//查询概览运营数据,提供给Excel模板文件
BusinessDataVO businessData = workspaceService.getBusinessData(LocalDateTime.of(begin,LocalTime.MIN), LocalDateTime.of(end, LocalTime.MAX));
InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("template/运营数据报表模板.xlsx");
try {
//基于提供好的模板文件创建一个新的Excel表格对象
XSSFWorkbook excel = new XSSFWorkbook(inputStream);
//获得Excel文件中的一个Sheet页
XSSFSheet sheet = excel.getSheet("Sheet1");

sheet.getRow(1).getCell(1).setCellValue(begin + "至" + end);
//获得第4行
XSSFRow row = sheet.getRow(3);
//获取单元格
row.getCell(2).setCellValue(businessData.getTurnover());
row.getCell(4).setCellValue(businessData.getOrderCompletionRate());
row.getCell(6).setCellValue(businessData.getNewUsers());
row = sheet.getRow(4);
row.getCell(2).setCellValue(businessData.getValidOrderCount());
row.getCell(4).setCellValue(businessData.getUnitPrice());
for (int i = 0; i < 30; i++) {
LocalDate date = begin.plusDays(i);
//准备明细数据
businessData = workspaceService.getBusinessData(LocalDateTime.of(date,LocalTime.MIN), LocalDateTime.of(date, LocalTime.MAX));
row = sheet.getRow(7 + i);
row.getCell(1).setCellValue(date.toString());
row.getCell(2).setCellValue(businessData.getTurnover());
row.getCell(3).setCellValue(businessData.getValidOrderCount());
row.getCell(4).setCellValue(businessData.getOrderCompletionRate());
row.getCell(5).setCellValue(businessData.getUnitPrice());
row.getCell(6).setCellValue(businessData.getNewUsers());
}
//通过输出流将文件下载到客户端浏览器中
ServletOutputStream out = response.getOutputStream();
excel.write(out);
//关闭资源
out.flush();
out.close();
excel.close();

}catch (IOException e){
e.printStackTrace();
}
}

实际上就是:

1
2
3
4
5
6
//有一个response
HttpServletResponse response
//得到输出流
ServletOutputStream out = response.getOutputStream();
//写入输出流
excel.write(out);

九、Stream

Java的流式操作也是一个难点

1、样例1

1
2
3
4
5
List<GoodsSalesDTO> goodsSalesDTOList = orderMapper.getSalesTop10(beginTime, endTime);

goodsSalesDTOList.stream()
.map(GoodsSalesDTO::getName)
.collect(Collectors.toList());
  1. goodsSalesDTOList.stream()

    • goodsSalesDTOList 是一个包含多个 GoodsSalesDTO 对象的列表。stream() 方法将该列表转化为一个 Stream 对象,允许我们进行一系列的流式操作。
  2. .map(GoodsSalesDTO::getName)

    map 是一个中间操作,用来将流中的每个元素(在此为 GoodsSalesDTO 对象)转换成另一种类型的元素。

    • map() 是 Stream API 中的一个中间操作,它会对 Stream 中的每个元素应用指定的函数,并将结果映射为一个新的 Stream。

    • 在这里,GoodsSalesDTO::getName 是一个方法引用,等价于对每个 GoodsSalesDTO 对象调用 getName() 方法。getName() 方法应该是 GoodsSalesDTO 类的一个成员方法,用于获取该对象的 name 属性。

    • 这一步的目的是从每个 GoodsSalesDTO 对象中提取出 name 字段。

  3. .collect(Collectors.toList())

    • collect() 是 Stream API 中的一个终结操作,它会将流中的元素收集到一个集合中。Collectors.toList() 是一个收集器,指示流中的元素应该被收集到一个 List 中。
    • 这一步会把通过 map() 提取出来的 name 字段收集到一个新的 List<String> 中。

这段代码的执行结果是:从 goodsSalesDTOList 中提取出每个 GoodsSalesDTO 对象的 name 属性,生成一个新的 List<String>,其中包含了所有商品的名称。

2、样例2

1
2
3
Map<String, CourseCategoryTreeDto> mapTemp = courseCategoryTreeDtos.stream()
.filter(item -> !id.equals(item.getId()))
.collect(Collectors.toMap(key -> key.getId(), value -> value, (key1, key2) -> key2));
  • 功能:这段代码通过流处理(stream)将 courseCategoryTreeDtos 列表转换为一个 Map,键是每个节点的 id,值是对应的节点对象 CourseCategoryTreeDto,同时排除了根节点(即排除掉 id 等于某个特定值的节点)。

    关键点

    • filter(item -> !id.equals(item.getId())):排除掉根节点,假设根节点是通过 id 来识别的。

    • collect(Collectors.toMap(...)):将处理后的数据收集到一个 Map 中,键为节点的 id,值为该节点对象。

    • (key1, key2) -> key2:这是一个合并函数,当遇到相同的 id 时,后来的节点将覆盖之前的节点。通常情况下,不会发生这种情况,但合并函数仍然需要提供。