Java – SpringBoot 项目搭建说明
简介
本文讲解,一个SpringBoot项目从0开始搭建时,需要引入哪些包,和做何种配置,保持更新。
项目结构说明
单包项目
单包项目是指创建一个主项目,所有的开发都在主项目中进行,包括控制器、服务、数据库映射等所需要的类,都在同一个包中实现,其总体结构如下:
项名名称
--主项目名称
----src
------main
--------java
----------cn.unsoft
------------config -> 用于保存SpringMVC项目配置的目录
------------controller -> web访问控制器
------------exception -> 异常处理配置目录
------------expression -> 自定义权限管理目录
------------filter -> 过滤器目录
------------handler -> 处理器目录
------------mapper -> Mybatis(Plus) 的映射文件目录
------------service -> 服务目录
------------model -> 实体类,DTO, VO,POJO等的目录
------------utils -> 工具类目录
--------resource
----------application.yml -> 项目总配置文件
----------mapper -> mapper XML 文件资源目录
------test
模块化项目
模块化项目与单包项目不同,模块化项目是由一个项目,包含多个包项目,我们称为模块,多个模块中分别负责整个项目中不同的功能,如server模块则负责MVC逻辑操作,pojo模块只负责与实体类相关的,如存放实体类,或对实体类进行加工的模块等。
模块化项目一般比较常用于企业开发中,同一个项目中,可以分出多个模块同时分工开发,所以在一般的项目中,建议使用模块化项目。实际上模块化项目中的模块可以看作是我们引用依赖的包,只是这个依赖包是我们自己开发的,而不是第三方依赖罢了。
项名名称
--模块名称1
----src
--模块名称2
----src
--模块名称3
----src
--...
pom依赖配置
要搭建一个SpringBoot项目,pom依赖是必不可少的,因为pom中是否拥有SpringBoot的依赖决定了你的项目是否能使用SpringBoot来启动应用。
前言
对于单体项目而言,pom文件就只有一个,就是当前包的pom,所以依赖配置就写在该pom文件中就好了。
对于模块化项目而言,pom文件就不止一个了,它分别是每一个模块下有一个pom文件,该pom文件只负责对当前的模块产生作用,还有一个pom文件位于整个项目的根目录中,这一个pom文件能对所有模块产生作用。
因此,我们可以在公共pom文件中,加入可能每个模块都需要使用的依赖,这样所有的模块都能引用,但对于一些不需要所有模块都用得上的依赖,我们可以把这些引用,放置在需要用得上这个依赖的模块pom文件中。
引入SpringBoot依赖
要使用SpringBoot来启动项目,就必须要引入SpringBoot依赖,其坐标如下:
<parent>
<artifactId>spring-boot-starter-parent</artifactId>
<groupId>org.springframework.boot</groupId>
<version>2.7.3</version>
</parent>
注意:要使用SpringBoot依赖,我们需要把pom文件的父依赖设为SpringBoot的pom依赖,原因是,SpringBoot集成了各种各样的整合接入,如我们想让redis整合进SpringBoot项目中时,SpringBoot实际已经提供了官方的pom依赖,此时我们把 spring-boot-starter-parent 作为父pom 时,我们就可以随时引入由SpringBoot提供的其它整合依赖了。
Q:为什么SpringBoot要提供大多数技术的整合pom?
A:因为每一种技术都由长期开发发展而来,每一种技术都有非常多的版本可供选择,但并不是所有技术的任意版本,都对任意SpringBoot版本兼容,如果由开发者自己去寻找对应兼容的技术版本进行整合,那将是非常麻烦的事,因此SpringBoot为了节省开发者对版本选择的问题,以专业的角度,对每一个SpringBoot版本挑选最兼容的技术整合软件版本,这样开发者只要选好了SpringBoot版本后,就能直接使用由Spring整理的技术兼容版本软件,也防止因整合技术不兼容而引发意外问题。目前SpringBoot已整合了大部分常用的技术整合,但对某些技术依然没有整合进去,这些没有被整合的技术,目前依然需要开发者自己尝试兼容性。
版本统一化
为了方便每一个依赖设置版本时做统一管理,我们可以在pom文件的 properties 中统一设置依赖版本,如下:
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<mybatis.spring>2.2.0</mybatis.spring>
<lombok>1.18.20</lombok>
<fastjson>1.2.76</fastjson>
<commons.lang>2.6</commons.lang>
<druid>1.2.1</druid>
<pagehelper>1.3.0</pagehelper>
<aliyun.sdk.oss>3.10.2</aliyun.sdk.oss>
<knife4j>3.0.2</knife4j>
<aspectj>1.9.4</aspectj>
<jjwt>0.9.1</jjwt>
<jaxb-api>2.3.1</jaxb-api>
<poi>3.16</poi>
</properties>
这样我们就可以统一管理依赖版本,在依赖引入中,可以使用格式引用properties中的值
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok}</version>
</dependency>
常用依赖引入
整理日常一个项目中所需要的技术依赖
MyBatis
SpringBoot对Mybatis有需要自己的依赖整合,可以使用以下坐标引入SpringBoot专用的依赖
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
Mybatis-Plus
Mybatis-Plus 是基于Mybatis而开发的增强型Mybatis插件,在保留Mybatis原有的功能外,还增强了很多方面的功能,目前基本采用Mybatis-Plus作为数据操作插件,以代替Mybatis
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
Lombok
Lombok是用于对实体类,或Bean自动创建get,set方法,或构造方法的插件,可以省略手工创建get/set麻烦
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
fastjson
fastjson 是用于java对象转为json对象时比较重要的依赖,它可以使前端传入的json转为java对象,同时也能让java对象转为json对象
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
common-lang
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
</dependency>
druid
druid 是 阿里巴巴开发的一款JDBC数据源软件,通过对JDBC的实现的高性能数据源,通常都可以使用它作为数据库数据源。
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
pagehelper
pagehelper 是应用在Mybatis中的一款分页插件,因为Mybatis的分页功能需要第三方插件支持,因此如果使用Mybatis作为数据库操作插件的话,就要安装pagehelper插件实现分页
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
</dependency>
knife4j
knife4j是一款辅助api接口文档生成插件,方便在开发过程中使用的
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
</dependency>
aspectj
aspectj 是一款面向切面编程辅助的函数代理软件
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
jwt
jwt是java后端用于生成token的其中一种解决方案,可以通过生成token判断是那一个用户,从而分配权限
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jjwt}</version>
</dependency>
阿里云oss
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
</dependency>
微信支付
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-apache-httpclient</artifactId>
</dependency>
Redis
Redis 的Spring Data Redis 依赖,专用于SpringBoot
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
HttpClient
HttpClient工具包是Apache下的一个子项目,用于提供服务器的Http协议的客户端编程工具包
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
Spring Security
使用 Spring Security 可以实现用户登陆机制,和授权功能
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Spring Cache
Spring Cache 是一个框架,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能
Spring Cache 提供了一层抽象,底层可又切换不同的缓存实现,例如:
EHCache
Caffeine
Redis
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
Apache Poi
Apache Poi 是用于操作office文档的导入导出功能的插件依赖
<!-- poi 支持 office 03版旧版本格式 -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
</dependency>
<!-- poi 支持 office 07版新版本格式 -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
</dependency>
统一返回对象
对于一个规范项目而言,后端的输出也需要有一个规范的格式,我们所有的结果输出给前端时,都需要有一个规范,方便前后端调用,因此我们在返回转化json对象方面,需要统一使用一个类来规范格式。
类的名称可以自定义,可以为简便的《R<>》对象,或《Result<>》对象,统一返回对象可以放在result包中,也可以放在util包中
统一返回对象代码一
package cn.unsoft.result;
import lombok.Data;
import java.io.Serializable;
/**
* 后端统一返回结果
* @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<T> result = new Result<>();
result.msg = msg;
result.code = 0;
return result;
}
}
存放属性对象
很多时候我们在yml文件中自定义了一些属性,而我们在程序开发时又需要使用到这些属性,我们可以通过 @Value 注册来直接获取,但是这种方法比较麻烦,所以在一个项目中,如果有多项自定义配置属性时,我们就可以创建一些类,用于保存yml中的属性,以方便我们在日常开发中调用。该类可以存放在 properties 包中。
如jwt的属性设置:
sky:
jwt:
# 设置jwt签名加密时使用的秘钥
admin-secret-key: itcast
# 设置jwt过期时间
admin-ttl: 7200000
# 设置前端传递过来的令牌名称
admin-token-name: token
这时我们就可以使用以下类记录下属性,以便在必要时说用类即可。
package cn.unsoft.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
// 读取 yml 文件中的 sky->jwt 节点中的属性数据
@ConfigurationProperties(prefix = "sky.jwt")
@Data
public class JwtProperties {
/**
* 管理端员工生成jwt令牌相关配置
*/
private String adminSecretKey;
private long adminTtl;
private String adminTokenName;
/**
* 用户端微信用户生成jwt令牌相关配置
*/
private String userSecretKey;
private long userTtl;
private String userTokenName;
}
要注意的是,类必须要是Bean对象,且添加@ConfigurationProperties对象以告诉SpringBoot这是一个存储属性的类,它会通过匹配节点来自动注入属性值,相关yml属性配置可查看相关资料
JSON转换规则类
SpringBoot默认对java对象可以转换为JSON对象,但在某些情况下,SpringBoot的转换规则并不满足于我们的需要,该类可以存在json包中
如:当SpringBoot对localTime,localDateTime等类型的数据时,它会对数据进舻数组化,即现实效果为:
2023-08-01 12:00:00 在转为JSON对象后,则会变成 [2023,8,1,12,0,0] 这样的数组,显然不是我们想要的格式,所以我们需要对某些特定的类型转为JSON时,需要按照我们的规范进行转化,
我们可以创建一个JSON转换对象,并定义哪些类型需要格式化,如下:
package cn.unsoft.json;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;
/**
* 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
* 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
* 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]
*/
public class JacksonObjectMapper extends ObjectMapper {
// 年月日的转换格式,针对 LocalDate 对象
public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
// 年月日时分秒的转换格式,针对 LocalDateTime 对象
//public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
// 年月日时分的转换格式,针对 LocalDateTime 对象
public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm";
// 时分秒的转换格式,针对 LocalTime 对象
public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";
// 创建一个转换对象
public JacksonObjectMapper() {
super();
//收到未知属性时不报异常
this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);
//反序列化时,属性不存在的兼容处理
this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
// 对序列化为JSON时,和JSON反序列化为Java时,都要求对应的转换格式
SimpleModule simpleModule = new SimpleModule()
.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))
.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
//注册功能模块 例如,可以添加自定义序列化器和反序列化器
this.registerModule(simpleModule);
}
}
注册到SpringBoot系统中
创建完转化类后,我们需要把该类实例化并载入SpringBoot系统中,以便生效,该类型为SpringBoot的 extendMessageConverters 消息类型转化扩展,在 WebMvcConfigurationSupport 中,对其重写即可。
/**
* 配置类,注册web层相关组件
*/
@Configuration
public class WebMvcConfiguration extends WebMvcConfigurationSupport {
/**
* 对SpringMVC中的数据进行统一格式化处理
* 如对日期类的对象转为json时的格式转换对象
* 也可以在单个实体类字段中加入 @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss") 注解来解决时间格式问题
*
* @param converters
*/
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
// 创建一个消息转化器对象
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
// 设置对象转换器,可以将java对象转为json字符串
converter.setObjectMapper(new JacksonObjectMapper());
// 把对象转换器加入到SpringMVC框架中
converters.add(0, converter);
}
}
异常处理
异常处理在项目中是非常重要的一部分,它是对整个系统不规范操作或程序错误后的兜底操作,
1.在一般的控制器请求中,每一个请求对应的url都是固定的,但如果用户故意访问本不存在的url时(或该用户无权限访问该api时),SpringBoot会输出500或404等错误,但格式并不规范,为了让这类不规范的操作也能接收并产生规范的报错输出,应当在项目中完善异常处理的事情。
2.在一些程序在执行过程中,难免会产生这样或那样的错误,这时如果因此发生系统崩溃那将是致命的,需要有异常进行兜底,不至于影响其它操作。
SpringBoot提供捕获异常的功能,通过在配置类中,增加注解【@RestControllerAdvice
】后,则会被认为,该类是处理异常的类,配合【@ExceptionHandler
】来定位异常精度问题。
全局异常处理
全局异常处理则是应用在整个程序中避免未知错误时所使用的,如系统报错等,都可以被捕获,通常全局异常的异常捕获范围是最大的,通常是 RuntimeException
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 捕获业务异常
* @param ex
* @return
*/
@ExceptionHandler
public Result exceptionHandler(RuntimeException ex){
log.error("异常信息:{}", ex.getMessage());
return Result.error(ex.getMessage()); // 自定义返回json格式给前端
}
}
特定异常处理
如果所有的错误,都被跟到全局异常处理的话,那在处理异常时会显得特别不方便,且返回的错误信息也不够准确,我们可以通过创建特定的异常类,让SpringBoot捕获特定异常,这样我们就可以对某些特定的异常进行特别处理,无需起到全局异常处理。方法也很简单,注解【@ExceptionHandler
】可以写入异常类,这样该异常处理方法只会捕获到特定的异常,而不会起到全局异常处理方法中。
1.创建一个特定异常,举例子,一个《账号不存在异常》
/**
* 账号不存在异常
*/
public class AccountNotFoundException extends RuntimeException {
public AccountNotFoundException() {
}
public AccountNotFoundException(String msg) {
super(msg);
}
}
2.在异常配置类中,加入特定的异常处理方法:
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 用户不存在异常
* @param ex
* @return
*/
@ExceptionHandler(AccountNotFoundException.class)
public Result<String> accountSaveException(RuntimeException ex){
log.error("异常信息:{}", ex.getMessage());
return Result.error(ex.getMessage());
}
}
这样,该方法只会捕获产生AccountNotFoundException异常时,才会被调用,如果我们所报出的异常,都没有被特定异常方法所捕获,那么最终异常会被全局异常处理方法处理。
我们可以创建多个这样的特定异常处理类,用于在不同的场境下,抛出不同的异常。
独立用户数据
在一个系统的运行过程中,有大量的用户对系统进行访问,但是系统要如何识别,这些用户谁是谁呢?
首先我们要确定的是,请求系统的用户,需要携带token,否则我们会认为该用户是一个未登陆的匿名用户。关于token请看token配置章节和拦截器章节
以下方法只选取其一就可以。
Session保存用户数据
我们可以通过往Session中存入用户数据,在特定需要鉴定用户的时候,只需要在Session中取回数据即可,因为Session对于每个用户请求而言是独立的,在Session中存入的数据,只有当前用户才可以获得,别人的数据,只能取回他自己的数据。
//todo
Redis保存用户数据
Redis虽然不是独立的,但是它是一个KV数据库,我们可以把用户的数据以KV形式保存在Redis中,当然前提是,每个用户保存在Redis中的Key需要唯一,此时SpringBoot就可以通过解密token后得到用户id,再往Redis中获取对应的用户详细信息。
// todo
ThreadLocal保存用户数据
ThreadLocal是Java提供的一种线程隔离存储技术,使用ThreadLocal存储的数据,只能在当前线程下才能获取到,其它线程只能获取到它自己的数据,如果我们把用户数据存储到ThreadLocal中时,就能在当前线程执行到任意代码时调用获取用户数据。
package cn.unsoft.context;
/**
* 用于保存当前用户的id,用于识别访问者是谁
* ThreadLocal 应用于每一条线程中,每个访问请求者都会独立占有一条线程
* 而在ThreadLocal中保存数据,仅会使该请求者能访问到自己的数据,用于存取访问者当前的id
*/
public class BaseContext {
public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
public static void setCurrentId(Long id) {
threadLocal.set(id);
}
public static Long getCurrentId() {
return threadLocal.get();
}
public static void removeCurrentId() {
threadLocal.remove();
}
}
常量值
在系统开发过程中,会避免不了使用固定字符串(魔法字符串)的情况,如果我们直接把字符串写死在代码中,会非常影响后续的维护开发工作,为了防止这类事情的发生,我们可以在项目中创建一个包,专门用于保存用于存储常量值的类,它们仅仅是为了把固定字符串写成常量值,在业务代码中,调用该常量值变量,而不是直接在代码中使用字符串。
为了规范系统开发,建议所有字符串都应该作为常量值调用,保存在constant包中
package cn.unsoft.constant;
/**
* 信息提示常量类
*/
public class MessageConstant {
public static final String PASSWORD_ERROR = "密码错误";
public static final String ACCOUNT_NOT_FOUND = "账号不存在";
public static final String ACCOUNT_LOCKED = "账号被锁定";
public static final String UNKNOWN_ERROR = "未知错误";
public static final String USER_NOT_LOGIN = "用户未登录";
public static final String CATEGORY_BE_RELATED_BY_SETMEAL = "当前分类关联了套餐,不能删除";
public static final String CATEGORY_BE_RELATED_BY_DISH = "当前分类关联了菜品,不能删除";
public static final String SHOPPING_CART_IS_NULL = "购物车数据为空,不能下单";
public static final String ADDRESS_BOOK_IS_NULL = "用户地址为空,不能下单";
public static final String LOGIN_FAILED = "登录失败";
public static final String UPLOAD_FAILED = "文件上传失败";
public static final String SETMEAL_ENABLE_FAILED = "套餐内包含未启售菜品,无法启售";
public static final String PASSWORD_EDIT_FAILED = "密码修改失败";
public static final String DISH_ON_SALE = "起售中的菜品不能删除";
public static final String SETMEAL_ON_SALE = "起售中的套餐不能删除";
public static final String DISH_BE_RELATED_BY_SETMEAL = "当前菜品关联了套餐,不能删除";
public static final String ORDER_STATUS_ERROR = "订单状态错误";
public static final String ORDER_NOT_FOUND = "订单不存在";
}
JWT生成Token
项目中需要登陆机制,当用户登陆系统后,要如何确认该用户是已登陆用户呢?这时需要使用token机制,通过配置JWT生成加密的token文本,对那些已登陆的用户生成特有的token文本发送给用户,用户访问必要的请求时,就需要提供token给系统进行校验,校验通过后的用户才允许访问特定控制器方法。
创建生成解密token的JWT类
创建一个工具类,用于生成和解密token,我们可以把用户必要的数据加进到JWT中加密成一个密文发给用户,每当用户请求时,我们获取来身用户提供的token进行解密得到该用户的必要信息,从而判断该用户是否拥有该请求的访问权。
package cn.unsoft.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Map;
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;
}
}
SpingMVC配置
一个系统项目中,需要提供多种自定义配置,该配置可以通过重写 WebMvcConfigurationSupport
类实现配置自定义,常用的自定义配置如下:
配置拦截器
请查看拦截器章节。
配置静态页面访问
SpringBoot默认对所有的请求都会被拦截并匹配控制器,但很多时候,有些访问的页面,本身不需要SpringBoot来接收匹配,如静态页面。
如果不配置,则我们在访问静态页面时,SpringBoot会以为我们访问的是控制器接口,从而产生访产异常,所以我们需要告诉SpringBoot,哪些请求是不需要经过它的拦截的。
/**
* 设置静态资源映射
* 对Swagger系统做静态映射,否则访问swagger时会被SpringMVC拦截,当成controller方法执行了
*
* @param registry
*/
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}
当然,如果我们的系统是前后端分离的话,本身SpringBoot项目中就没有静态资源时,我们可以不用配置。
JSON格式化统一处理
即对java转为json对象时的自定义格式化工作,详情查阅 JSON转换规则类
拦截器
拦截器是一个相对重要的配置步骤,它的意义也非常大,拦截器可以对进来的请求进行拦截加工。
常见的功能如:
1.解密token,判断用户是否已登陆或token是否失效,从而设置该用户的访问权限定义,给后面的鉴权提供铺垫
2.SpringSecurity的权限控制
要实现拦截器,拦截器类需要实现 HandlerInterceptor
接口方法,HandlerInterceptor
接口中提供了三个接口方法,分别是
preHandle : 调用控制器方法之前触发,多数用这个方法对数据进行预先处理,再放给控制器
postHandle : 控制器方法执行完成之后触发
afterCompletion : 对视图进行渲染完成后的回调方法,说白了就是所有控制器操作都完成了之后,会回调一次这个方法
JWT权限验证拦截器
当用户登陆后,系统会给用户一个token,而当已登陆的用户请求访问时,用户会在header头中携带token文给到SpringBoot。这时,在SpringBoot触发控制器方法之前,就应该要先验证token的合法性,这时我们就需要创建一个拦截器,拦截在控制器方法之前,对token进行验证,
代码如下:
package cn.unsoft.interceptor;
import cn.unsoft.constant.JwtClaimsConstant;
import cn.unsoft.properties.JwtProperties;
import cn.unsoft.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* jwt令牌校验的拦截器
*/
@Component
@Slf4j
public class JwtTokenAdminInterceptor implements HandlerInterceptor {
// 用于保存yml文件中的配置属性的类
@Autowired
private JwtProperties jwtProperties;
/**
* 校验jwt
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//判断当前拦截到的是Controller的方法还是其他资源
if (!(handler instanceof HandlerMethod)) {
//当前拦截到的不是动态方法,直接放行
return true;
}
//1、从请求头中获取令牌
String token = request.getHeader(jwtProperties.getAdminTokenName());
//2、校验令牌
try {
log.info("jwt校验:{}", token);
// 使用JWT工具类解密得到密文中的数据,token有可能不是合法的,则会跳到401错误
Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
// 我们可以在生成token时存入多个字段用户信息,在解密后也可以取出多个字段数据,当前只存入了用户id
Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
log.info("当前员工id:", empId);
//3、通过,放行
return true;
} catch (Exception ex) {
//4、不通过,响应401状态码
response.setStatus(401);
return false;
}
}
}
JWT拦截器加入到SpringBoot中
创建完拦截器后,我们需要把拦截器加入到系统中,才会对拦截器生效
/**
* 配置类,注册web层相关组件
*/
@Configuration
@Slf4j
public class WebMvcConfiguration extends DelegatingWebMvcConfiguration {
@Resource
private JwtTokenAdminInterceptor jwtTokenAdminInterceptor;
/**
* 注册自定义拦截器
*
* @param registry
*/
protected void addInterceptors(InterceptorRegistry registry) {
log.info("开始注册自定义拦截器...");
// 对jwt拦截器设置生效
registry.addInterceptor(jwtTokenAdminInterceptor)
// 设置对拦截器生效的路径
.addPathPatterns("/admin/**")
// 设置除去生效的路径
.excludePathPatterns("/admin/employee/login");
}
}
MVC之控制器
控制器是整个系统中对外提供api接口的类,我们所访问的每一个请求url中,都是由控制器提供服务的,但控制器严格来说只负责对外开放url,其实际业务逻辑不建议在控制器中过多的体现
使用 @RestController
注解则会被SpringBoot认为该类是控制器类,@RestController
实际是 @ResponseBody
和@Controller
的合体,其中@ResponseBody
代表我们方法返回后是以json型式输出,如果没有@ResponseBody
,则SpringBoot会认为返回的结果(主要是字符串)将当作为JSP页面名称,并进行页面渲染。而 @Controller
则是把该类给Spring进行管理。
简单代码如下:
@RestController
@RequestMapping("/hello/out")
public class HelloController {
@GetMapping("/get")
public Result<String> hello(){
return Result.success("ok");
}
}
@RequestMapping
则是声明该控制器负责的url请求总路径
@GetMapping
则是表示声明要触发访方法的调用,需要使用GET方法访问,总的路径是/hello/out/get
三种传递参数说明
网页中传递数据有三种方式,分别是query、path、json
query
query是指在网址后添加参数的方式,如下例子:
https://127.0.0.1/hello/out/get?user=admin&pass=123456
其中的user=admin 和 pass=123456 则是query参数
SpringBoot中接收query参数
通过使用注解 @RequestParam("")
定义在形参上,如果@RequestParam("user")
,则说明,接收到的query参数对应的user的值会被赋给该注解下的形参
当然,SpringBoot为了方便开发者的使用,对于query参数而言,@RequestParam
注解可以忽略不用,但形参中的名字,必须和query中的参数名一致。
path
path是指在网址中的下级深度,如下例子:
https://127.0.0.1/hello/out/get/admin/1
其中的 admin 和 1 则是 path值,因为out和get已经被 @RequestMapping
和 @GetMapping
所占有,所以不能看作是接收的path参数
path和query不一样的地方在于,query是Key-Value结构,而path只有Value,因此如果我们需要在SpringBoot中接收path参数,需要使用占位符 {} 来声明
通过@PathVariable
注解,声明哪一个path值,传给哪一个形参
@RestController
@RequestMapping("/hello/out")
public class HelloController {
@GetMapping("/{user}/{id}")
public Result<String> hello(@PathVariable("user") String user,@PathVariable("id") Long id){
return Result.success("ok");
}
}
json
如果当请求提交的数据比较多时,通过使用query和path是不现实的,如注册账号的情况,需要提供的表单比较多,这时就需要使用json结构化数据传递。
json传数据,不能使用在GET方法中,多出现在POST方法中
使用 @RequestBody
把json数据转化为Java对象数据,可以把json转为Map数据,也可以转为实体类数据。
MVC之服务
服务是主要处理业务逻辑的地方,控器器在接收到请求后,会把需要处理的业务提交给服务进行后台处理。
主要分为 interface 接口类和 实现类。此处略。
MVC之数据库映射
数据映射是系统与数据库数据接接的重要一环,它分为两个点。
POJO实体类
POJO是实体类的一个大类,其分类下还有3个小类,分别是
model实体类
model实体类则是一种与数据库字段完全吻合的一种实体类,它作为数据表的映射使用。
DTO实体类
DTO是前端提交的数据经过转化后给后端使用的阉割实体类。可以看作,DTO是SpringBoot接收前端使用的接收实体类
VO实体类
VO与DTO相反,VO是由后端提供的准备转化给前端使用的数据的阉割实体类。可以看作,VO是后端发给前端使用的数据实体类。
Mapper
Mybatis 的工作方式是,利用接口,对数据库操作进行约束,而查询数据库的具体实现则可以通过两种方式进行
接口中的注解
如果SQL语句不是特别复杂,也没有做联表查询等复杂操作时,我们可以通过直接在Mapper接口中定义数据库查询SQL
@Mapper
public interface Mapper {
/**
* @param id
* @return
*/
@Select("select count(id) from setmeal where category_id = #{categoryId}")
Integer countByCategoryId(Long id);
}
@Select
传入一个查询SQL语句,实现单表的查询功能
@Insert
传入一个SQL语句,实现单表的插入功能
@Update
传入一个SQL语句,实现单表的修改功能
@Delete
传入一个SQL语句,实现单表删除某行数据的功能
注意,如果Mapper方法中,只接收一个参数时,SQL语句中的变量不会作出限制,如上面的代码,虽然接收的形参是 "id",但因为方法中,有且仅有一个参数,此时Mybatis并不限制SQL代码中的参数取用,即使它使用 #{categoryId} 来接收也是被允许的
但是如果Mapper方法中出现了2个以上的形参时,就不可以这样了,需要使用 @Param("") 去声明那个形参对应那个查询值了,如下代码:
@Select("select count(id) from setmeal where category_id = #{categoryId}")
Integer countByCategoryId(@Param("categoryId") Long id,@Param("id") Long a);
此时的,Long a 是不会被使用的,因为使用了 @Param 声明了,Long id 是应用在categoryId上的
XML文件
当SQL出现比较复杂时,需要进行联表查询,甚至联库查询的时候,仅仅通过注解是不能解决的,这个时候就需要使用到XML文件定义查询SQL语句了。
xml文件默认需要与Mapper接口文件的目录结构和层决必须相同,如Mapper接口存在于 cn/unsoft/mapper 中时,XML文件也必须要放置在 cn/unsoft/mapper 中.
但包中的文件,在编译时,会过滤掉非java文件,所以XML文件可以存放在Resource文件夹中,并且其文件夹结构也与Mapper接口文件夹结构相同,如 Resource/cn/unsoft/mapper 中
当然,也可以通过yml中设置指定的mapperXML文件的保存地方,可以使用以下配置值进行配置
mybatis:
#mapper配置文件
mapper-locations: classpath:mapper/*.xml
#把xml文件中的包类型设置别名,这样所有的xml中的类型就不需要精确地定位包的位署,将默认定位到指定包位置中寻找类型
type-aliases-package: cn.unsoft.entity
configuration:
#开启驼峰命名,即数据库中的字段如“user_name”可以被实体类中的 UserName 所识别
map-underscore-to-camel-case: true
Mybatis 使用建议
字段自动填充
Mybatis本身并没有自动填充功能,所以我们需要手动的创建注解以便在插入和修改的Mapper方法中加入这个注解,实现切面编程,增强该Mapper方法,对Mapper方法中的字段自动填充工作。
1.创建一个自定义的注解
/**
* 自定义注解
* 用于标记数据库插入与修改的方法进行切面编程
* 对于标记此注解的方法中,对特定的数据库字段进行填充
*/
@Target(ElementType.METHOD) // 注解可使用的范围
@Retention(RetentionPolicy.RUNTIME) // 注解可使用的运行时机
public @interface AutoFill {
OperationType value();
}
2.创建一个切面类,用于针对打上注解的方法进行增强,也就是自动加工帮填充
package cn.unsoft.aspect;
import cn.unsoft.annotation.AutoFill;
import cn.unsoft.constant.AutoFillConstant;
import cn.unsoft.context.BaseContext;
import cn.unsoft.enumeration.OperationType;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
@Component
@Slf4j
@Aspect
public class AutoFillAspect {
// * [任意返回值] cn.unsoft.mapper.*.*[cn.unsoft.mapper包中的任意类的任意方法] (..)[任意参数] 的方法
// &&
// @annotation(cn.unsoft.annotation.AutoFill) [针对标记了AutoFill注解的方法]
@Pointcut("execution(* cn.unsoft.mapper.*.*(..)) && @annotation(cn.unsoft.annotation.AutoFill)")
public void AutoFillPointCut() {
}
// 关于获取 @annotation 变量的方法,可以使用先定义注解形参,再在方法中接收注解
// @Before(value = "@annotation(注解变量名)")
// public void xxx(JoinPoint jointPoint , 自定义注解类型 注解变量名)
// 如下,我创建了一个 Log 类型的注解,然后可以使用另名来在形参中获取注解
@Before(value = "@annotation(sysLog)") // 两者名称相同时,@annotation就知道你要获取的是哪个注解
public void xxx(JoinPoint jointPoint , Log sysLog)
@Before("AutoFillPointCut()")
public void FillField(JoinPoint joinPoint) {
// 取得触发切面的方法的签名,因为它是一个方法,所以要强制转为方法签名
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// 取得触发切面的方法的传入参数
Object[] args = joinPoint.getArgs();
// 获取当前用户id
Long userId = BaseContext.getCurrentId();
// 获取当前时间
LocalDateTime now = LocalDateTime.now();
// 取出这个方法签名所在的指定注解
AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);
// 取得传进来的参数的对象
Object obj = args[0];
// 如果这个注解中的值为 INSERT
if (autoFill.value() == OperationType.INSERT) {
//取出这个参数对象里的set方法,以备后面使用
try {
// 获得该形参对象中的针对需要填充的字段的set方法
Method setCreateTime = obj.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
Method setCreateUser = obj.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
Method setUpdateTime = obj.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
Method setUpdateUser = obj.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
// 执行set方法,并把数据载进去,实现反射技术设置数据
setCreateTime.invoke(obj, now);
setCreateUser.invoke(obj, userId);
setUpdateTime.invoke(obj, now);
setUpdateUser.invoke(obj, userId);
} catch (Exception e) {
throw new RuntimeException(e);
}
} else if (autoFill.value() == OperationType.UPDATE) {
try {
Method setUpdateTime = obj.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
Method setUpdateUser = obj.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
setUpdateTime.invoke(obj, now);
setUpdateUser.invoke(obj, userId);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
3.切面做好后,只需要在Mapper方法中,加入 @AutoFill 注解,就可以实现自动填充功能。
@AutoFill(OperationType.UPDATE)
void update(Employee employee);
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="cn.unsoft.mapper.SetmealDishMapper">
<!-- 示例代码,具体的标签使用可查看Mybatis相关文章 -->
<select id="getSetmealDishIdsFromIds" resultType="java.lang.Long">
select count(setmeal_id) from setmeal_dish
<where>
dish_id in
<foreach collection="ids" item="id" separator="," open="(" close=")">
#{id}
</foreach>
</where>
</select>
</mapper>
Mybatis-Plus 使用建议
字段自动填充
MP使用字段自动填充非常简单,MP本身提供了一个注解 @TableField
,只需要在实体类的成员变量前增加注解@TableField(fill= ?)
自动填充时机即可。
填充时机有四种,分别如下:
DEFAULT,
INSERT, // 插入时填充
UPDATE, // 修改时填充
INSERT_UPDATE; // 插入和修改时都填充
/**
* MyBatisPlus 配置类项
* <p>
* insertFill 是 MyBatisPlus 提供的字段填充功能,可以填充插入数据库时,某些字段自动填充
* updateFill 是 MyBatisPlus 提供的字段填充功能,可以填充更新数据库时,某些字段自动填充
* 自动填充,需要在实体类的成员中加上 @TableField(fill) 注解
*/
@Configuration
public class MyBatisPlusConfig implements MetaObjectHandler {
/**
* 对某些字段设定自动数据填充功能
* @param metaObject 由 MyBatisPlus 提供的当前正在处理的数据行中的元数据
* 包含了当前正在处理的数据行的实体类对象等信息
* 包含了处理该数据行的 条件 信息
* 可以在这个metaObject对象中处理需要加工的字段
* 当metaObject被提交出去之后,MP 将按照 metaObject中
* 所提供的实体类数据进行数据库写入操作
*/
@Override
public void insertFill(MetaObject metaObject) {
Long userId = null;
try {
// 如果取不到UserId,说明这个是一个注册账号的请求
userId = SecurityUtils.getUserId();
} catch (Exception e) {
e.printStackTrace();
userId = -1L;//表示是自己创建
}
this.setFieldValByName("createTime", new Date(), metaObject);
this.setFieldValByName("createBy", userId, metaObject);
this.setFieldValByName("updateTime", new Date(), metaObject);
this.setFieldValByName("updateBy", userId, metaObject);
}
@Override
public void updateFill(MetaObject metaObject) {
this.setFieldValByName("updateTime", new Date(), metaObject);
this.setFieldValByName("updateBy", SecurityUtils.getUserId(), metaObject);
}
/**
* 配置数据库事务管理器
*/
@Autowired
private DataSource dataSource;
@Bean
public TransactionManager transactionManager(){
return new DataSourceTransactionManager(dataSource);
}
}
分页插件功能
可以整合到字段自动填充配置类中
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
}
Redis
Spring Data Redis
Spring Data Redis 是Spring用于对Redis功能的封装,是Spring的一部分,对Spring程序兼容比较好
导入坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
创建RedisTemplate连接对象
package cn.unsoft.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
@Slf4j
public class RedisConfiguration {
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate redisTemplate = new RedisTemplate(){{
// 设置 redis 的连接工厂对象
setConnectionFactory(redisConnectionFactory);
// 设置redis key 的序列化器,通过设置后,Redis的key字段以字符串存储,否则存储的是序列化后的对象
setKeySerializer(new StringRedisSerializer());
// 设置 redis value 的序列化器,否则Value会存入java序例化后的对象
setValueSerializer(new StringRedisSerializer());
}};
return redisTemplate;
}
}
在yml文件中对Redis进行配置
spring:
redis:
host: ${sky.redis.host} # 通过分配置文件的方式载入属性值
port: ${sky.redis.port}
password: ${sky.redis.password}
database: ${sky.redis.database}
在需要使用Redis读写时,就可以通过注入 RedisTemplate 对象进行读写
// 常用的Redis类型操作
@Resource
private RedisTemplate redisTemplate;
redisTemplate.opsForValue(); // String 类型
redisTemplate.opsForHash(); // Table 哈唏类型
redisTemplate.opsForList(); // List 列表类型
redisTemplate.opsForSet(); // Set 无序列集合
redisTemplate.opsForZSet(); // ZSet或SortSet 有序列集合
HTTPClient
HTTPClient 是Apache的子项目,用于在服务器中对http请求进行编程操作,可以在代码中实现http请求。
GET请求实例
public void GETTest() throws Exception {
// 1. 创建 HTTPClient对象,HttpClients是一个接口
CloseableHttpClient client = HttpClients.createDefault();
// 2. 创建 HTTP GET 请求对象
HttpGet httpGet = new HttpGet("http://localhost:8080/user/shop/status");
httpGet.setHeader("token", "xxxxxxxx");
// 3. 使用 HTTPClient 对 GET 请求进行访问
CloseableHttpResponse response = client.execute(httpGet);
// 4. 取出的即是请求后的 response
int statusCode = response.getStatusLine().getStatusCode();
System.out.println("响应状态码:" + statusCode);
// 注意,HTTPClient 把响应内容封装成了HttpEntity,可以通过 getContent 获取流来获得内容
// 也可以使用 EntityUtils 工具类把请求转为文本
HttpEntity entity = response.getEntity();
String content = EntityUtils.toString(entity);
System.out.println("响应内容:" + content);
}
POST请求实例
public void testPost() throws Exception {
// 1. 创建HTTPClient对象,HttpClients是一个接口
CloseableHttpClient client = HttpClients.createDefault();
// 2. 创建 POST 对象
HttpPost httpPost = new HttpPost("http://localhost:8080/admin/login");
// 3. 对于 post 而言,它需要提供参数,可以通过setEntity来设置要传递的参数
// HttpEntity 是一个接口,有26个实现类,我们使用 StringEntity 作为参数类型
JSONObject jsonObject = new JSONObject(){{
put("username","admin");
put("password","123456");
}};
StringEntity stringEntity = new StringEntity(jsonObject.toString());
// 设置参数解码编码与数据类型
stringEntity.setContentEncoding("utf-8");
stringEntity.setContentType("application/json");
httpPost.setEntity(stringEntity);
// 4. 对post请求访问
CloseableHttpResponse response = client.execute(httpPost);
// 5. 取出的即是请求后的 response
int statusCode = response.getStatusLine().getStatusCode();
System.out.println("响应状态码:" + statusCode);
// 注意,HTTPClient 把响应内容封装成了HttpEntity,可以通过 getContent 获取流来获得内容
// 也可以使用 EntityUtils 工具类把请求转为文本
HttpEntity entity = response.getEntity();
String content = EntityUtils.toString(entity);
System.out.println("响应内容:" + content);
}
HTTPClientUtils 工具类
package cn.unsoft.utils;
import com.alibaba.fastjson.JSONObject;
import org.apache.http.NameValuePair;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Http工具类
*/
public class HttpClientUtil {
static final int TIMEOUT_MSEC = 5 * 1000;
/**
* 发送GET方式请求
* @param url
* @param paramMap
* @return
*/
public static String doGet(String url,Map<String,String> paramMap){
// 创建Httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
String result = "";
CloseableHttpResponse response = null;
try{
URIBuilder builder = new URIBuilder(url);
if(paramMap != null){
for (String key : paramMap.keySet()) {
builder.addParameter(key,paramMap.get(key));
}
}
URI uri = builder.build();
//创建GET请求
HttpGet httpGet = new HttpGet(uri);
//发送请求
response = httpClient.execute(httpGet);
//判断响应状态
if(response.getStatusLine().getStatusCode() == 200){
result = EntityUtils.toString(response.getEntity(),"UTF-8");
}
}catch (Exception e){
e.printStackTrace();
}finally {
try {
response.close();
httpClient.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return result;
}
/**
* 发送POST方式请求
* @param url
* @param paramMap
* @return
* @throws IOException
*/
public static String doPost(String url, Map<String, String> paramMap) throws IOException {
// 创建Httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = null;
String resultString = "";
try {
// 创建Http Post请求
HttpPost httpPost = new HttpPost(url);
// 创建参数列表
if (paramMap != null) {
List<NameValuePair> paramList = new ArrayList();
for (Map.Entry<String, String> param : paramMap.entrySet()) {
paramList.add(new BasicNameValuePair(param.getKey(), param.getValue()));
}
// 模拟表单
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(paramList);
httpPost.setEntity(entity);
}
httpPost.setConfig(builderRequestConfig());
// 执行http请求
response = httpClient.execute(httpPost);
resultString = EntityUtils.toString(response.getEntity(), "UTF-8");
} catch (Exception e) {
throw e;
} finally {
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return resultString;
}
/**
* 发送POST方式请求
* @param url
* @param paramMap
* @return
* @throws IOException
*/
public static String doPost4Json(String url, Map<String, String> paramMap) throws IOException {
// 创建Httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = null;
String resultString = "";
try {
// 创建Http Post请求
HttpPost httpPost = new HttpPost(url);
if (paramMap != null) {
//构造json格式数据
JSONObject jsonObject = new JSONObject();
for (Map.Entry<String, String> param : paramMap.entrySet()) {
jsonObject.put(param.getKey(),param.getValue());
}
StringEntity entity = new StringEntity(jsonObject.toString(),"utf-8");
//设置请求编码
entity.setContentEncoding("utf-8");
//设置数据类型
entity.setContentType("application/json");
httpPost.setEntity(entity);
}
httpPost.setConfig(builderRequestConfig());
// 执行http请求
response = httpClient.execute(httpPost);
resultString = EntityUtils.toString(response.getEntity(), "UTF-8");
} catch (Exception e) {
throw e;
} finally {
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return resultString;
}
private static RequestConfig builderRequestConfig() {
return RequestConfig.custom()
.setConnectTimeout(TIMEOUT_MSEC)
.setConnectionRequestTimeout(TIMEOUT_MSEC)
.setSocketTimeout(TIMEOUT_MSEC).build();
}
}
Spring Security
在大多数项目中,权限管理是必不可少的,通过使用Spring Security可以方便地控制用户的登陆和授权功能,而且是Spring家的分支,可以完美支持SpringBoot项目
配置
Spring Security 引入后,会自动注入过滤器,我们可以通过继承 Security 配置对象,来对系统做自定义的安全配置
@Configuration
// 继承 WebSecurityConfigurerAdapter 重写方法,配置自定义设置
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* SpringSecurity 的默认密码加密类,可以使用它作为密码加密工具
* 也可以使用其它加密方式如md5等等,但需要实现 PasswordEncoder 接口方法
* @return
*/
@Bean
public PasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* 配置放行
* 说明:对于 SpringSecurity 而言,它是不知道哪些请求需要做拦截认证,哪些请求不需要
* 所以当我们发送 login 请求时,不应该让 SpringSecurity 拦截
* 这里是指当在访问某些页面请求时,应该让 SpringSecurity 放行
*
* @param http the {@link HttpSecurity} to modify
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//关闭csrf
.csrf().disable()
//不通过Session获取SecurityContext,不开启session机制,通常是前后端分离项目时使用
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 对于登录接口 允许匿名访问
.antMatchers("/user/login").anonymous()
//用配置的方式配置某个请求的权限校验功能 -> 对 /test 请求的权限控制范围要求必须有 system::user::list 的用户才可以访问
.antMatchers("/test").hasAnyAuthority("system::user::list")
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
// 增加一个自定义的过滤器,用于拦截 Token 并对 token 进行验证,当然,这里的过滤器要放在 userpass 过滤器之前
http.addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class);
// 配置认证和授权失败时的异常处理方法
http.exceptionHandling()
// 处理认证失败时的异常
.authenticationEntryPoint(authenticationEntryPoint)
// 处理授权失败时的异常
.accessDeniedHandler(accessDeniedHandler);
// 开启跨域请求,解决跨域请求问题
http.cors();
// 配置认证成功处理器
// http.formLogin().successHandler(authenticationSuccessHandler);
// http.formLogin().failureHandler(authenticationFailureHandler);
// http.logout().logoutSuccessHandler(logoutSuccessHandler);
}
}
注意:规则的顺序是重要的,更具体的规则应该先写.
HttpSecurity配置列表
方法 | 说明 |
openidLogin() | 用于基于 OpenId 的验证 |
headers() | 将安全标头添加到响应 |
cors() | 配置跨域资源共享( CORS ) |
sessionManagement() | 允许配置会话管理 |
portMapper() | 允许配置一个PortMapper(HttpSecurity#(getSharedObject(class))),其他提供SecurityConfigurer的对象使用 PortMapper 从 HTTP 重定 向到 HTTPS 或者从 HTTPS 重定向到 HTTP。默认情况下,Spring Security使用一个PortMapperImpl映射 HTTP 端口8080到 HTTPS 端口 8443,HTTP 端口80到 HTTPS 端口443 |
jee() | 配置基于容器的预认证。 在这种情况下,认证由Servlet容器管理 |
x509() | 配置基于x509的认证 |
rememberMe | 允许配置“记住我”的验证 |
authorizeRequests() | 允许基于使用HttpServletRequest限制访问,可以在其下设置路径的授权功能,如下:
http.authorizeRequests().antMatchers("/admin/**").hasAuthority("p1") |
requestCache() | 允许配置请求缓存 |
exceptionHandling() | 允许配置错误处理 |
securityContext() | 在HttpServletRequests之间的SecurityContextHolder上设置SecurityContext的管理。 当使用WebSecurityConfigurerAdapter时,这将 自动应用 |
servletApi() | 将HttpServletRequest方法与在其上找到的值集成到SecurityContext中。 当使用WebSecurityConfigurerAdapter时,这将自动应用 |
csrf() | 添加 CSRF 支持,使用WebSecurityConfigurerAdapter时,默认启用 |
logout() | 添加退出登录支持。当使用WebSecurityConfigurerAdapter时,这将自动应用。默认情况是,访问URL”/ logout”,使HTTP Session无效来 清除用户,清除已配置的任何#rememberMe()身份验证,清除SecurityContextHolder,然后重定向到”/login?success” |
anonymous() | 允许配置匿名用户的表示方法。 当与WebSecurityConfigurerAdapter结合使用时,这将自动应用。 默认情况下,匿名用户将使用 org.springframework.security.authentication.AnonymousAuthenticationToken表示,并包含角色 “ROLE_ANONYMOUS” |
formLogin() | 指定支持基于表单的身份验证。如果未指定FormLoginConfigurer#loginPage(String),则将生成默认登录页面 |
oauth2Login() | 根据外部OAuth 2.0或OpenID Connect 1.0提供程序配置身份验证 |
requiresChannel() | 配置通道安全。为了使该配置有用,必须提供至少一个到所需信道的映射 |
httpBasic() | 配置 Http Basic 验证 |
addFilterAt() | 在指定的Filter类的位置添加过滤器 |
认证
Spring Security 分为认证和授权两大部分,认证是指需要对该用户进行身份验证,是认证系统的第一个门槛,控制只能使已登陆用户才能访问的某些权限。
认证流程说明:
1.最开始由 UsernamePasswordAuthenticationFilter
过滤器进行收集用户和密码信息(UsernamePasswordAuthenticationFilter
只是接口其中一个实现)
2.UsernamePasswordAuthenticationFilter
提交 authenticate
方法给AuthenticationManager
并由AuthenticationManager
转发给DaoAuthenticationProvider
来执行(DaoAuthenticationProvider
是整个流程中最主要的执行者,它包含了接收认证任务,和提交认证后的信息的执行流程)
3.DaoAuthenticationProvider
不会一次认证用户名和密码,而是先认证用户名,查看是否存在用户名,会调用 UserDetailService
接口中的 loadUserByUsername()
方法,要想自定义获取用户名方式,可以通过重写loadUserByUsername()
方法,在数据库中获取用户信息。
4.loadUserByUsername()
方法要求返回UserDetails
对象,其实UserDetails
对象就是一个抽象的带有用户账号密码和授权信息的对象,我们可以通过查询数据库后,取出用户的用户、密码、拥有的权限等信息存入UserDetails
对象中,也可重写UserDetails
对象,以增加属于自己的自定义信息。(注意,认证方式并非只有用户密码这一种,所以Authentication
中对用户密码的描述并不是 Username 和 Password)
5.通过返回的UserDetails
给DaoAuthenticationProvider
后,便会知道这个用户名是否存在(如果不存在,可以把UserDetails
认为null,会自动抛出无用户名异常,后面可以自定义报错)
6.DaoAuthenticationProvider
会对UserDetails
中的认证信息进行认证,如果使用了加密,会通过加密密文认证,认证成功会填充Authentication
对象给UsernamePasswordAuthenticationFilter
7.UsernamePasswordAuthenticationFilter
可以获取到认证成功后的Authentication
,然后把Authentication
写入到SecurityContextHolder
中。
授权
授权是指用户已经登陆成功,但对于系统而言,不同的用户具有不同的权限,什么样的用户只能用什么样的功能是可以明确规定的,Spring Security 同样使用了过滤器对用户访问进行了拦截处理。
授权可以分为两种,一种是路径授权,一种是方法授权
路径授权是指,设置某请求路径下要求用户拥有指定的权限才能访问,如下方法可设置路径授权:
// 要求访问 /admin/ 下的所有请求,用户都必需具有 admin 或 manager 权限(关于【和】的权限可以使用access("hasAuthority('admin') and hasAuthority('manager')"))
http.authorizeRequests().antMatchers("/admin/**").hasAnyAuthority("admin","manager");
保护URL常用的方法有:
authenticated() 保护URL,需要用户登录
permitAll() 指定URL无需保护,一般应用与静态资源文件
hasRole(String role) 限制单个角色访问,角色将被增加 “ROLE_” .所以”ADMIN” 将和 “ROLE_ADMIN”进行比较.
hasAuthority(String authority) 限制单个权限访问
hasAnyRole(String… roles) 允许多个角色访问.
hasAnyAuthority(String… authorities) 允许多个权限访问.
access(String attribute) 该方法使用 SpEL表达式, 所以可以创建复杂的限制.
hasIpAddress(String ipaddressExpression) 限制IP地址或子网
方法授权是指,通过在某些访问的控制器方法上加上注解,声明该方法只有存在指定权限才能被访问
方法如下:
1.开启注册授权
@Configuration
// 开启授权管理
@EnableGlobalMethodSecurity(prePostEnabled = true)
// 继承 WebSecurityConfigurerAdapter 是为了方便重写它的方法获得某些对象
public class SecurityConfig extends WebSecurityConfigurerAdapter { }
其中prePostEnabled中一类授权方式,需要指定开启,才能在方法中使用,另外授权方式还有 securedEnabled = true
prePostEnabled的授权说明:
// 设置匿名访问
@PreAuthorize("isAnonymous()")
public Account readAccount(Long id);
// 设置特定的授权访问,这里需要同时存在 system:admin:post 和 system:admin:manager 两个权限才能访问该方法
@PreAuthorize("hasAuthority('system:admin:post') and hasAuthority('system:admin:manager')")
public Account post(Account account, double amount);
}
secured的授权说明:
// 允许匿名访问
@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account readAccount(Long id);
// 设置拥有角色ADMIN的用户可以访问,ROLE_不能省略,比较少用
@Secured("ROLE_ADMIN")
public Account post(Account account, double amount);
}
Spring Cache
当用注解:
@EnableCaching
- 开启缓存注解功能,通常加在启动类上
@Cacheable
- 在方法执行前先查询缓存中是否有数据,如果有数据则直接返回缓存数据;如果没有缓存数据,调用方法并将方法返回值放到缓存中
@CachePut
- 将方法的返回值放到缓存中
@CacheEvict
- 将一条或多条数据从缓存中删除
@Cacheable 参数说明
@Cacheable() 注解可以传入参数,用于区分缓存,拿Redis举例,Redis使用的是Key-Value模式存储数据,那么Key可以自定义
cacheNames -> 它是组成Cache的key的前缀,我们知道,Redis没有数据库的慨念,因此数据全部都存在一个库中,如何使这些数据区分开来,则需要使用key前缀来区分,而cacheNames则是这个key的前缀,可以写多个
key -> 用于区分Cache分类下的唯一key值,如User类中的UserId,则可以使用 key 传入
@Cacheable 会形成格式为【cacheName::key】的形式组合 key 值。而value则是输出结果。
SpEL表达式
针对上面的key并非是一个固定值,而是由方法体中的某个变量所决定的,如 UserId 是不一样的,需要让方法体中的值传给 key ,可以使用 Spring 提供的 SpEL表达式传递。
方式一:直接传入方法体中的同名变量名:
@Cacheable(cacheNames = "User",key = "#categoryId")
public Result list(@RequestParam("categoryId") Long categoryId) { }
@Cacheable(cacheNames = "User",key = "#user.id")
public Result list(@RequestParam("categoryId") User user) { }
使用 # 号开头,并引入形参名
方式二:使用 #p 或 #a 获得形参值:
@Cacheable(cacheNames = "User",key = "#p0")
public Result list(@RequestParam("categoryId") Long categoryId) { }
@Cacheable(cacheNames = "User",key = "#p0.id")
public Result list(@RequestParam("categoryId") User user) { }
其中 p0 或 a0 表示第0个形参值
方式三:使用 #root.args[0] 获得形参值:
@Cacheable(cacheNames = "User",key = "#root.args[0]")
public Result list(@RequestParam("categoryId") Long categoryId) { }
@Cacheable(cacheNames = "User",key = "#root.args[0].id")
public Result list(@RequestParam("categoryId") User user) { }
其中 args[0] 表示第0个形参值
方式四:使用 #result 获取方法体的返回结果值(@Cacheable不能用)
@Cacheable(cacheNames = "User",key = "#result.id")
public Result list(@RequestParam("categoryId") Long categoryId) { }
在SpringBoot中配合Redis使用Spring Cache 中,对Redis的配置
/**
* 使用Redis配合SpringCache对Redis进行配置
* 如过期时间、json序列化等
*/
@Configuration
public class RedisCacheConfig {
@Bean
public CacheManager cacheManager(LettuceConnectionFactory connectionFactory) {
//定义序列化器
GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
//过期时间600秒
.entryTtl(Duration.ofSeconds(600))
// 配置序列化
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(stringRedisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(genericJackson2JsonRedisSerializer));
RedisCacheManager cacheManager = RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.build();
return cacheManager;
}
}
Spring Task 定时任务
使用方法
1.导入 坐标,位于 Spring-context 包中,在Spring-boot-start 中已包含
2.开启任务调度 @EnableScheduling
注解在启动类中
3.创建自定义定时任务类,在里面设定定时任务即可。
@Component
public class TaskRun {
@Scheduled(cron = "* * * * * ?")
public void task(){
}
}
Cron表达式
cron表达式其实就是一个字符串,通过cron表达式可以定义任务触发的时间
构成规则:分为6或7个域,由空格分隔开,每个域代表一个含义
每个域的含义分别为:秒、分钟、小时、日、月、周、年(可选)
秒
秒可使用通配符:【,】、【-】、【*】、【/】
【,】 -> 指定某几个时间段,如 1,2,3 表示,指定第一,第二,第三秒都执行一次
【-】 -> 指定周期时间,如 1-10 表示,从1到10秒每秒都会执行一次
【*】 -> 不指定时间,取决于其它时段触发
【/】 -> 指定每隔多少执行,如 5/10 表示,从第5秒开始,每隔10秒执行一次
分钟
分钟可使用通配符:【,】、【-】、【*】、【/】
【,】 -> 指定某几个时间段,如 1,2,3 表示,指定第一,第二,第三分钟都执行一次
【-】 -> 指定周期时间,如 1-10 表示,从1到10分钟每分钟都会执行一次
【*】 -> 不指定时间,取决于其它时段触发
【/】 -> 指定每隔多少执行,如 5/10 表示,从第5分钟开始,每隔10分钟执行一次
小时
小时可使用通配符:【,】、【-】、【*】、【/】
【,】 -> 指定某几个时间段,如 1,2,3 表示,指定第一,第二,第三小时都执行一次
【-】 -> 指定周期时间,如 1-10 表示,从1到10点每小时都会执行一次
【*】 -> 不指定时间,取决于其它时段触发
【/】 -> 指定每隔多少执行,如 0/1 表示,从第0点开始,每隔1小时执行一次
日
日可使用通配符:【,】、【-】、【*】、【/】、【L】、【W】
日与周互斥,使用了日,就不能使用周,若使用了周,则日需要用【?】代替
【,】 -> 指定某几个时间段,如 1,2,3 表示,指定第一,第二,第三天都执行一次
【-】 -> 指定周期时间,如 1-10 表示,从1到10天每天都会执行一次
【*】 -> 不指定时间,取决于其它时段触发
【?】 -> 本段时间无效
【/】 -> 指定每隔多少执行,如 1/2 表示,从第1一开始,每隔2天执行一次
【L】 -> 指定本月最后一天,如6月最后一天是30号
【W】 -> 指定每月几号最近的工作日,如4号是周六,则最近的工作日是3号周五
月份
月份可使用通配符:【,】、【-】、【*】、【/】
【,】 -> 指定某几个时间段,如 1,2,3 表示,指定第一,第二,第三月都执行一次
【-】 -> 指定周期时间,如 1-10 表示,从1到10月每月都会执行一次
【*】 -> 不指定时间,取决于其它时段触发
【/】 -> 指定每隔多少执行,如 1/2 表示,从第1号开始,每隔2个月执行一次
周(星期)
周可使用通配符:【,】、【-】、【*】、【/】、【L】、【#】
周与日互斥,使用了周,就不能使用日,若使用了日,则周需要用【?】代替
【,】 -> 指定某几个时间段,如 1,2,3 表示,指定星期一、星期二、星期三都执行一次
【-】 -> 指定每隔多少执行,如 1-5 表示,每周从周一到周五都会执行一次
【*】 -> 不指定时间,取决于其它时段触发
【?】 -> 本段时间无效
【/】 -> 指定周期时间,如 1-3 表示,从星期一到星期三都执行一次
【#】 -> 指定第几周特定日子时段,如 1#2 表示,指定每月的第2周的第一天(即每个月第二周的周日)
【L】 -> 指定本月最后一个星期的某天,如 1L 表示,指定本月的最后一个星期一
年份(非必填)
年可使用通配符:【,】、【-】、【*】
【,】 -> 指定某几个时间段,如 2023,2024 表示,指定2023年和2024年都执行一次
【-】 -> 指定周期时间,如 2023-2025 表示,从2023年到2025年都会执行一次
【*】 -> 不指定时间,取决于其它时段触发
Cron 常用例子
(1)0/2 * * * * ? 表示每2秒 执行任务
(1)0 0/2 * * * ? 表示每2分钟 执行任务
(1)0 0 2 1 * ? 表示在每月的1日的凌晨2点调整任务
(2)0 15 10 ? * MON-FRI 表示周一到周五每天上午10:15执行作业
(3)0 15 10 ? 6L 2002-2006 表示2002-2006年的每个月的最后一个星期五上午10:15执行
(4)0 0 10,14,16 * * ? 每天上午10点,下午2点,4点
(5)0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时
(6)0 0 12 ? * WED 表示每个星期三中午12点
(7)0 0 12 * * ? 每天中午12点触发
(8)0 15 10 ? * * 每天上午10:15触发
(9)0 15 10 * * ? 每天上午10:15触发
(10)0 15 10 * * ? 每天上午10:15触发
(11)0 15 10 * * ? 2005 2005年的每天上午10:15触发
(12)0 * 14 * * ? 在每天下午2点到下午2:59期间的每1分钟触发
(13)0 0/5 14 * * ? 在每天下午2点到下午2:55期间的每5分钟触发
(14)0 0/5 14,18 * * ? 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
(15)0 0-5 14 * * ? 在每天下午2点到下午2:05期间的每1分钟触发
(16)0 10,44 14 ? 3 WED 每年三月的星期三的下午2:10和2:44触发
(17)0 15 10 ? * MON-FRI 周一至周五的上午10:15触发
(18)0 15 10 15 * ? 每月15日上午10:15触发
(19)0 15 10 L * ? 每月最后一日的上午10:15触发
(20)0 15 10 ? * 6L 每月的最后一个星期五上午10:15触发
(21)0 15 10 ? * 6L 2002-2005 2002年至2005年的每月的最后一个星期五上午10:15触发
(22)0 15 10 ? * 6#3 每月的第三个星期五上午10:15触发
WebSocket
WebSocket 是基于 TCP 的一种新的网络协议。它实现了浏览器与服务器全双工通信一浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输
导入Maven
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
配置 WebSocket 到SpringBoot中
/**
* 配置WebSocket
*/
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}
WebSocket使用案例
@Component
@Slf4j
@ServerEndpoint("/ws/{id}")
public class WebSocketServer {
private static Map<String, Session> sessionMap;
static {
{
sessionMap = new HashMap<>();
}
}
/**
* 当客户端使用WS连接到服务器时
*
* @param session
* @param sid
*/
@OnOpen
public void onOpen(Session session, @PathParam("id") String sid) {
log.info("客户端连接WS:" + sid);
sessionMap.put(sid, session);
}
/**
* 当客户端向WS服务器发送消息时
*/
@OnMessage
private void onMessage(String message, @PathParam("id") String id) {
log.info("客户端发送消息来:" + message + "由客户端" + id);
}
/**
* 当客户端关闭WS时
* @param session
* @param sid
*/
@OnClose
private void onClose(Session session, @PathParam("id") String sid) {
log.info("客户端关闭连接:" + sid);
sessionMap.remove(sid);
}
/**
* 向所有连接的客户端发送信息
* @param message
*/
public void sendAllMessage(String message) {
Collection<Session> sessions = sessionMap.values();
sessions.forEach(session -> {
try {
session.getBasicRemote().sendText("向客户端发送消息");
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
}
Apache POI
Apache POI 是用于 MS Office Excel 文档操作的api依赖包
写XLS
写excel 文档的顺序,从高到低,分别是 excel -> sheet -> row(行) -> cell(单元格)
通过这种顺序,设置单元格的数据,以下代码:
public static void main(String[] args) throws IOException {
// 1.创建一个 Excel 对象
XSSFWorkbook excel = new XSSFWorkbook();
// 2.在这个Excel 对象中创建一个 Sheet,因为Excel 对象默认没有Sheet表
XSSFSheet sheet = excel.createSheet("sheetName");
// 3.在这个sheet对象中创建一行,注意,行从0开始算
XSSFRow row = sheet.createRow(0);
// 4.在这个单元行中,定位到具体的单元格,并进行填充数据,单元格从0开始算
row.createCell(0).setCellValue("第一行数据1");
row.createCell(1).setCellValue("第一行数据2");
// 5.按照 3. 和 4. 步骤可得,操作其它行其它格可用同样方法
row = sheet.createRow(1);
row.createCell(0).setCellValue("第二行数据1");
row.createCell(1).setCellValue("第二行数据2");
// 6.把表格保存到磁盘中
FileOutputStream out = new FileOutputStream("D:\\info.xlsx");
excel.write(out);
// 7.关闭文档流和输出流
excel.close();
out.close();
}
读XLS
public static void main(String[] args) throws Exception {
// 1.创建一个 Excel 对象,对象构造可以传入一个文件地址,或输入流,则说明这是操作文件,而非内存创建
XSSFWorkbook excel = new XSSFWorkbook(new File("d:\\info.xlsx"));
// 2.读取Excel对象中的Sheet表
XSSFSheet sheet = excel.getSheet("sheetName");
// 3.在sheet中可以获取到最后一行有数据的行,来判定我们的表格数据到底有多少行,根据行数来逐一读取
int rowNum = sheet.getLastRowNum();
// 4.通过循环获逐一获取单元格中的数据
for (int i = 0; i <= rowNum; i++) {
// 5.获得每一行的数据
XSSFRow row = sheet.getRow(i);
// 6.在当前行中获取对应的单元格数据,单元格从0开始算
String v1 = row.getCell(1).getStringCellValue();
String v2 = row.getCell(2).getStringCellValue();
System.out.println(v1 + " " + v2);
}
// 7.关闭工作簿
excel.close();
}
共有 0 条评论