SpringBoot项目合集
SpringBoot项目合集
1、搭建SpringBoot项目
导入依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>sixkey</groupId>
<artifactId>swx</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.1</version>
</parent>
<dependencies>
<!-- Mvc依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<!--mysql数据库驱动依赖-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-jdbc -->
<!--一定要引入一个jdbc依赖,不然会和springboot默认的冲突-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<version>2.7.3</version>
</dependency>
<!--druid连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.22</version>
</dependency>
<!--简化配置-->
<!-- 引入lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- 引入fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.7</version>
</dependency>
配置文件
server:
port: 8200
spring:
application:
name: swx
datasource:
url: jdbc:mysql://localhost:3306/sixkey?characterEncoding=UTF-8&useUnicode=true&useSSL=false&serverTimezone=Asia/Shanghai&tinyInt1isBit=false&allowPublicKeyRetrieval=true
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: '020708'
type: com.alibaba.druid.pool.DruidDataSource
initial-size: 10
max-active: 150
min-idle: 10
max-wait: 5000
pool-prepared-statements: false
validation-query: SELECT 1
validation-query-timeout: 500
test-on-borrow: false
test-on-return: false
test-while-idle: true
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 30000
filters: stat,wall,log4j
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
2、集成swagger增强版
添加依赖
<!-- swagger-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<!--knife4j-spring-boot-starter-->
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
添加配置文件
#knife4j
knife4j:
# 开启增强配置
enable: true
basic:
enable: true
# Basic认证用户名
username: admin
# Basic认证密码
password: admin
添加配置类
package sixkey.config;
import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
/**
* ClassName:Knife4jConfig
* Package:sixkey.config
* Description
*
* @Author:@wenxueshi
* @Create:2023/5/19 - 22:26
* @Version:v1.0
*/
@Configuration
@EnableSwagger2
@EnableKnife4j
public class Knife4jConfig {
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.useDefaultResponseMessages(false)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("sixkey.controller"))
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.description("文学接口文档")
.contact(new Contact("文学", "http:://8.130.108.52", "2073958890@qq.com"))
.version("v1.1.0")
.title("文学接口文档")
.build();
}
}
美化启动类
@Slf4j
@SpringBootApplication
@ServletComponentScan
public class Application {
public static void main(String[] args) throws UnknownHostException {
ConfigurableApplicationContext application = SpringApplication.run(Application.class, args);
log.info("==============项目启动成功==============");
//获取配置信息
ConfigurableEnvironment environment = application.getEnvironment();
//获取port
String port = environment.getProperty("server.port");
//获取ip主机
String ip = InetAddress.getLocalHost().getHostAddress();
System.out.println("\n----------------------------------------------------------\n\t" +
"sixkey is running! Access URLs:\n\t" +
"Knife4j-ui: \thttp://" + ip + ":" + port + "/doc.html\n\t" +
"----------------------------------------------------------");
}
}
控制台输出格式
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.4.1)
2023-05-19 22:40:32.367 INFO 16360 --- [ main] com.alibaba.druid.pool.DruidDataSource : {dataSource-1} inited
2023-05-19 22:40:32.463 INFO 16360 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8200 (http) with context path ''
2023-05-19 22:40:32.530 INFO 16360 --- [ main] sixkey.Application : Started Application in 2.473 seconds (JVM running for 3.058)
2023-05-19 22:40:32.531 INFO 16360 --- [ main] sixkey.Application : ==============项目启动成功==============
----------------------------------------------------------
sixkey is running! Access URLs:
Knife4j-ui: http://192.168.134.131:8200/doc.html //直接点即可访问接口文档
----------------------------------------------------------
controller类编写
@Api(tags = "系统用户管理接口")
@RestController
@RequestMapping("/users")
public class UserController {
@ApiOperation(value = "用户列表", httpMethod = "GET", notes = "用户列表")
@GetMapping("/test")
public String test(){
return "hello swx~";
}
}
浏览器访问展示

3、集成Mybatis-Plus
添加依赖
<!--mybatis-plus依赖,已经包括了mybatis的依赖了-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.1</version>
</dependency>
<!--mybatis-plus的模板引擎依赖,使用mybatyis-plus必须要加这个依赖-->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.0</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.3.2</version>
</dependency>
配置文件
#==============================mybatis-plus配置==============================
mybatis-plus:
mapper-locations: classpath:/mapper/*Mapper.xml
#实体扫描,多个package用逗号或者分号分隔
typeAliasesPackage: com.shiyi.entity
global-config:
# 数据库相关配置
db-config:
#主键类型 AUTO:"数据库ID自增", INPUT:"用户输入ID",ID_WORKER:"全局唯一ID (数字类型唯一ID)", UUID:"全局唯一ID UUID";
id-type: AUTO
#字段策略 IGNORED:"忽略判断",NOT_NULL:"非 NULL 判断"),NOT_EMPTY:"非空判断"
field-strategy: not_empty
#驼峰下划线转换
column-underline: true
db-type: mysql
db-type: mysql
logic-delete-field: flag # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
#刷新mapper 调试神器
refresh: true
# 原生配置
configuration:
map-underscore-to-camel-case: true
cache-enabled: false
logging:
level: warn
公共字段自动填充
- 编写MyMetaObjectHandler 类
- 实现MetaObjectHandler接口
package sixkey.config.MybatisPlusConfig;
/**
* ClassName:MyMetaObjectHandler
* Package:sixkey.config.MybatisPlusConfig
* Description
*
* @Author:@wenxueshi
* @Create:2023/5/19 - 23:15
* @Version:v1.0
*/
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.Date;
/**
* 公共字段自动填充配置
*/
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
/**
* 插入时自动填充
* @param metaObject
*/
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", Date.class, new Date());
this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date());
}
/**
* 修改时自动填充
* @param metaObject
*/
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date());
}
}
实体类
package sixkey.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import javax.annotation.Tainted;
import java.io.Serializable;
import java.util.Date;
/**
* ClassName:User
* Package:sixkey.entity
* Description
*
* @Author:@wenxueshi
* @Create:2023/5/19 - 23:20
* @Version:v1.0
*/
/**
* 系统用户类
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("sys_user") //对应数据库的表
@Accessors(chain = true) //可以链式编写
public class User implements Serializable {
@TableId // 主键
private Integer id;
private String username;
private String password;
private Integer age;
private String sex;
@TableLogic
private Integer deleted; //逻辑删除
@TableField(fill = FieldFill.INSERT) //插入时自动填充
private Date createTime;
@TableField(fill = FieldFill.INSERT_UPDATE) //修改时自动填充
private Date updateTime;
}
mapper类
/**
* 系统用户
*/
public interface UserMapper extends BaseMapper<User> {
}
service类
/**
* 系统用户类
*/
public interface IUserService extends IService<User> {
}
serviceImpl类
/**
* 系统用户类
*/
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
}
具体的CRUD由具体功能要求去实现吧~
4、统一响应基础类
响应枚举类
package sixkey.utils;
/**
* ClassName:ResCodeEnum
* Package:sixkey.utils
* Description
*
* @Author:@wenxueshi
* @Create:2023/5/20 - 0:11
* @Version:v1.0
*/
/**
* 响应码枚举类
*/
public enum ResCodeEnum {
SUCCESS(200,"操作成功"),
FAIL(201,"操作失败"),
NEED_LOGIN(401,"需要登录后操作"),
SYSTEM_ERROR(500,"系统错误"),
USERNAME_EXIST(501, "用户名已存在"),
PHONENUMBER_EXIST(502, "手机号已存在"),
EMAIL_EXIST(503, "邮箱已存在"),
REQUIRE_USERNAME(504, "必需填写用户名"),
CONTENT_NOT_NULL(506, "评论内容不能为空"),
FILE_TYPE_ERROR(507, "文件类型错误"),
USERNAME_NOT_NULL(508, "用户名不能为空"),
NICKNAME_NOT_NULL(509, "昵称不能为空"),
PASSWORD_NOT_NULL(510, "密码不能为空"),
EMAIL_NOT_NULL(511, "邮箱不能为空"),
NICKNAME_EXIST(512, "昵称已存在"),
LOGIN_ERROR(505, "用户名或密码错误");
int code;
String message;
ResCodeEnum(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode(){
return code;
}
public String getMessage(){
return message;
}
}
统一响应类
package sixkey.utils;
import java.io.Serializable;
/**
* ClassName:ResResult
* Package:sixkey.utils
* Description
*
* @Author:@wenxueshi
* @Create:2023/5/20 - 0:21
* @Version:v1.0
*/
/**
* 统一结果响应类
*/
public class ResResult<T> implements Serializable {
private Integer code;
private String message;
private T data;
//当操作成功,无返回数据时
public ResResult(){
this.code = ResCodeEnum.SUCCESS.code;
this.message = ResCodeEnum.SUCCESS.getMessage();
}
//返回码code和数据data
public ResResult(Integer code, T data){
this.code = code;
this.data = data;
}
//返回码code和消息message
public ResResult(Integer code, String message) {
this.code = code;
this.message = message;
}
//返回码code、消息message、数据集data
public ResResult(Integer code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}
public static ResResult errorResult(int code, String msg) {
ResResult result = new ResResult();
return result.error(code, msg);
}
public static ResResult okResult() {
ResResult result = new ResResult();
return result;
}
public static ResResult okResult(int code, String msg) {
ResResult result = new ResResult();
return result.ok(code, null, msg);
}
public static ResResult okResult(Object data) {
ResResult result = setAppHttpCodeEnum(ResCodeEnum.SUCCESS, ResCodeEnum.SUCCESS.getMessage());
if (data != null) {
result.setData(data);
}
return result;
}
public static ResResult errorResult(ResCodeEnum enums) {
return setAppHttpCodeEnum(enums, enums.getMessage());
}
public static ResResult errorResult(ResCodeEnum enums, String msg) {
return setAppHttpCodeEnum(enums, msg);
}
public static ResResult setAppHttpCodeEnum(ResCodeEnum enums) {
return okResult(enums.getCode(), enums.getMessage());
}
private static ResResult setAppHttpCodeEnum(ResCodeEnum enums, String msg) {
return okResult(enums.getCode(), msg);
}
public ResResult<?> error(Integer code, String msg) {
this.code = code;
this.message = msg;
return this;
}
public ResResult<?> ok(Integer code, T data) {
this.code = code;
this.data = data;
return this;
}
public ResResult<?> ok(Integer code, T data, String msg) {
this.code = code;
this.data = data;
this.message = msg;
return this;
}
public ResResult<?> ok(T data) {
this.data = data;
return this;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
- 成功
ResResult.okResult();
- 失败
ResResult.errorResult(ResCodeEnum.SYSTEM_ERROR) , 参数传入我们定义的响应枚举类
实战
package sixkey.controller;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import sixkey.domain.vo.UserVo;
import sixkey.service.IUserService;
import sixkey.utils.ResResult;
/**
* ClassName:controller
* Package:sixkey
* Description
*
* @Author:@wenxueshi
* @Create:2023/5/19 - 22:09
* @Version:v1.0
*/
@Slf4j
@Api(tags = "系统用户管理接口")
@RestController
@RequestMapping("/users")
//lombok提供的自动注入注解,但是被注入属性要加final修饰
@RequiredArgsConstructor
public class UserController {
//已经通过@RequiredArgsConstructor此注解自动注入值
private final IUserService userService;
/**
* 添加用户
* @param userVo
* @return
*/
@ApiOperation(value = "添加用户", httpMethod = "POST",response = ResResult.class, notes = "添加用户")
@PostMapping("/add")
public ResResult addUser(@RequestBody UserVo userVo){
log.info("==== {}用户请求进来了 =====",userVo.getUsername());
userService.addUser(userVo);
return ResResult.okResult();
}
}
接口测试展示

5、测试报错
报了 java.lang.IllegalArgumentException: argument type mismatch 数据类型不匹配
**原因:**数据库中的create_time 和 update_time设置了时间戳来填充,而mybatisplus自动填充字段那里是用了系统时间
修改数据库

**代码修改:**设置为当前时间
package sixkey.config.MybatisPlusConfig;
/**
* ClassName:MyMetaObjectHandler
* Package:sixkey.config.MybatisPlusConfig
* Description
*
* @Author:@wenxueshi
* @Create:2023/5/19 - 23:15
* @Version:v1.0
*/
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.Date;
/**
* 公共字段自动填充配置
*/
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
/**
* 插入时自动填充
* @param metaObject
*/
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", Date.class, new Date());
this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date());
}
/**
* 修改时自动填充
* @param metaObject
*/
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date());
}
}
6、统一异常处理
自定义一个系统异常去继承Exception异常类
/**
* 系统异常处理
*/
public class SystemException extends Exception{
private int code;
private String message;
public SystemException(ResCodeEnum resCodeEnum){
super(resCodeEnum.getMessage());
this.code = resCodeEnum.getCode();
this.message = resCodeEnum.getMessage();
}
public int getCode() {
return code;
}
@Override
public String getMessage() {
return message;
}
}
自定义全局异常处理类
package sixkey.Exception.handler;
/**
* ClassName:Handler
* Package:sixkey.Exception
* Description
*
* @Author:@wenxueshi
* @Create:2023/5/20 - 16:40
* @Version:v1.0
*/
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import sixkey.Exception.SystemException;
import sixkey.utils.ResCodeEnum;
import sixkey.utils.ResResult;
/**
* 统一异常处理
*/
@RestControllerAdvice //统一异常处理注解
@Slf4j
public class GlobalExceptionHandler {
//此注解可以捕捉到SystemException抛出的异常
@ExceptionHandler(SystemException.class)
public ResResult systemExceptionHandler(SystemException systemException){
log.info("出现了异常:",systemException);
return ResResult.errorResult(systemException.getCode(),systemException.getMessage());
}
//对未知异常进行一个兜底
@ExceptionHandler(Exception.class)
public ResResult exceptionHandler(Exception exception){
log.info("出现了异常:",exception);
return ResResult.errorResult(ResCodeEnum.SYSTEM_ERROR.getCode(),exception.getMessage());
}
}
7、SpringBoot参数校验
导入依赖
<!--参数校验-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
使用
package sixkey.domain.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.util.Date;
import java.util.UUID;
/**
* ClassName:User
* Package:sixkey.entity
* Description
*
* @Author:@wenxueshi
* @Create:2023/5/19 - 23:20
* @Version:v1.0
*/
/**
* 系统用户类
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("sys_user") //对应数据库的表
@Accessors(chain = true) //可以链式编写
public class User implements Serializable {
@TableId // 主键
private Integer id;
@NotBlank(message = "用户名不能为空")
private String username;
@NotBlank(message = "密码不能为空")
private String password;
@JsonIgnore //忽略前端传过来的值
private String salt = UUID.randomUUID().toString().replaceAll("-","");
@NotNull(message = "年龄不能为空")
private Integer age;
@NotBlank(message = "性别不能为空")
private String sex;
@TableLogic
@JsonIgnore
private Integer deleted; //逻辑删除
@TableField(fill = FieldFill.INSERT) //插入时自动填充
private Date createTime;
@TableField(fill = FieldFill.INSERT_UPDATE) //修改时自动填充
private Date updateTime;
}
注意:字符串和int类型的属性所使用的注解不同,否则校验不匹配。
controller添加注解 并在参数前添加注解@Valid
//参数校验
@Validated
public class UserController {
//已经通过@RequiredArgsConstructor此注解自动注入值
private final IUserService userService;
/**
* 用户注册
* @param user
* @return
*/
@ApiOperation(value = "用户注册", httpMethod = "POST",response = ResResult.class, notes = "用户注册")
@PostMapping("/registry")
public ResResult RegisterUser(@RequestBody @Valid User user){
log.info("==== {}用户请求进来了 =====",user.getUsername());
userService.RegisterUser(user);
return ResResult.okResult();
}
统一异常返回,这样前端好看
//参数校验异常
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResResult bindException(MethodArgumentNotValidException e){
log.info("出现了异常:",e);
return ResResult.errorResult(ResCodeEnum.SYSTEM_ERROR.getCode(),e.getBindingResult().getAllErrors().get(0).getDefaultMessage());
}

8、集成Sa-Token
导入依赖
<!-- Sa-Token 权限认证,在线文档:https://sa-token.cc -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.34.0</version>
</dependency>
添加配置文件
############## Sa-Token 配置 (文档: https://sa-token.cc) ##############
sa-token:
# token名称 (同时也是cookie名称)
token-name: swxToken
# token有效期,单位s 默认30天, -1代表永不过期
timeout: 2592000
# token临时有效期 (指定时间内无操作就视为token过期) 单位: 秒
activity-timeout: -1
# 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)
is-concurrent: true
# 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token)
is-share: true
# token风格
token-style: uuid
# 是否输出操作日志
is-log: true
登录认证
完成数据库比对后,添加核心代码
//3.登录 StpUtil.login(user.getId());
/**
* 用户登录
* @param loginUserVo
*/
@Override
public void doLogin(LoginUserVo loginUserVo) throws UserNameNotFoundException, PasswordErrorException {
//1.根据用户名查询是否有此用户
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUsername,loginUserVo.getUsername());
User user = baseMapper.selectOne(wrapper);
if(user == null){
//用户名不存在
throw new UserNameNotFoundException(ResCodeEnum.USERNAME_NOTFOUND);
}
//2.密码是否正确
String salt = user.getSalt();
String password = loginUserVo.getPassword();
String MD5Password = DigestUtils.md5DigestAsHex((password + salt).getBytes());
if(!MD5Password.equals(user.getPassword())){
throw new PasswordErrorException(ResCodeEnum.LOGIN_ERROR);
}
//3.登录
StpUtil.login(user.getId());
}
}
判断当前用户是否是登录状态
StpUtil.isLogin();
退出登录
StpUtil.logout();
查看用户Token信息
StpUtil.getTokenInfo()
// 查询登录状态,浏览器访问: http://localhost:8081/users/isLogin
@ApiOperation(value = "查询登录状态",httpMethod = "GET",response = String.class,notes = "查询登录状态")
@GetMapping("/isLogin")
public String isLogin() {
return "当前会话是否登录:" + StpUtil.isLogin();
}
// 查询登录状态,浏览器访问: http://localhost:8081/users/isLogin
@ApiOperation(value = "退出登录",httpMethod = "GET",response = ResResult.class,notes = "退出登录")
@GetMapping("/loginOut")
public ResResult loginOut() {
StpUtil.logout();
return ResResult.okResult();
}
// 查询Token信息 ---- http://localhost:8081/acc/tokenInfo
@ApiOperation(value = "查询Token信息",httpMethod = "GET",response = ResResult.class,notes = "查询Token信息")
@GetMapping("/tokenInfo")
public ResResult tokenInfo() {
return ResResult.okResult(StpUtil.getTokenInfo());
}
权限认证:基于注解鉴权
- @SaCheckLogin: 登录校验 —— 只有登录之后才能进入该方法。
- @SaCheckRole("admin"): 角色校验 —— 必须具有指定角色标识才能进入该方法。
- @SaCheckPermission("user:add"): 权限校验 —— 必须具有指定权限才能进入该方法。
- @SaCheckSafe: 二级认证校验 —— 必须二级认证之后才能进入该方法。
- @SaCheckBasic: HttpBasic校验 —— 只有通过 Basic 认证后才能进入该方法。
- @SaIgnore:忽略校验 —— 表示被修饰的方法或类无需进行注解鉴权和路由拦截器鉴权。
- @SaCheckDisable("comment"):账号服务封禁校验 —— 校验当前账号指定服务是否被封禁。
Sa-Token 使用全局拦截器完成注解鉴权功能,为了不为项目带来不必要的性能负担,拦截器默认处于关闭状态 因此,为了使用注解鉴权,你必须手动将 Sa-Token 的全局拦截器注册到你项目中
注册拦截器
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
// 注册 Sa-Token 拦截器,打开注解式鉴权功能
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册 Sa-Token 拦截器,打开注解式鉴权功能
registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");
}
}
获取当前账号权限码集合
/**
* 自定义权限验证接口扩展
*/
@Component // 保证此类被SpringBoot扫描,完成Sa-Token的自定义权限验证扩展
@RequiredArgsConstructor
public class StpInterfaceImpl implements StpInterface {
private final MenuMapper menuMapper;
private final RoleMapper roleMapper;
/**
* 返回一个账号所拥有的权限码集合
*/
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
return menuMapper.getMenuByUserId(loginId);
}
/**
* 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)
*/
@Override
public List<String> getRoleList(Object loginId, String loginType) {
return roleMapper.getRoleByUserId(loginId);
}
}
sql查询语句
查询操作权限路径
<?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="sixkey.mapper.MenuMapper">
<select id="getMenuByUserId" resultType="java.lang.String">
select auth_url from sys_menu
where id in(select menu_id from sys_role_menu
where role_id = (select role_id from sys_user_role where user_id=#{loginId}))
</select>
</mapper>
查询操作角色
<?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="sixkey.mapper.RoleMapper">
<select id="getRoleByUserId" resultType="java.lang.String">
SELECT role_name
from sys_role
WHERE id = (SELECT role_id from sys_user_role WHERE user_id = #{loginId});
</select>
</mapper>
controller层添加注解
/**
* 修改用户
* @param user
* @return
*/
@SaCheckLogin
@SaCheckRole("admin")
@SaCheckPermission("system/user/update")
@ApiOperation(value = "修改用户",httpMethod = "PUT",response = ResResult.class,notes = "修改用户")
@PutMapping("/update")
public ResResult updateUser(@RequestBody @Valid User user){
// 检验当前会话是否已经登录, 如果未登录,则抛出异常:`NotLoginException`
StpUtil.checkLogin();
log.info("修改用户请求进来了.............");
userService.updateUser(user);
return ResResult.okResult();
}
疑问?为什么加个简单的注解就可以实现鉴权呢?
因为我们在上面做了获取当前账号权限码集合,只要实现了StpInterface接口并做了查询,那么Sa-Token就帮我们做了一切。
9、短信验证登录
前期准备工作
登录阿里云官网—–>找到产品里面的短信服务,点进去,然后看到如下画面

一路走

添加签名


添加模板


获取AccessKey Id 和密码去

不再截图
点进去AccessKey管理,点开始使用子用户AccessKey,然后创建用户,登录名随便填,访问方式填:OpenAPI 调用访问
点进去刚刚创建的用户里面即可得到 id和密码
java核心代码及工具类
导入maven依赖
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.5.16</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.aliyun/aliyun-java-sdk-dysmsapi -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-dysmsapi</artifactId>
<version>2.1.0</version>
</dependency>
两个工具类
/**
* 短信发送工具类
*/
public class SMSUtils {
/**
* 发送短信
* @param signName 签名
* @param templateCode 模板
* @param phoneNumbers 手机号
* @param param 参数
*/
public static void sendMessage(String signName, String templateCode,String phoneNumbers,String param){
DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", "", "");//填短信服务id和密码
IAcsClient client = new DefaultAcsClient(profile);
SendSmsRequest request = new SendSmsRequest();
request.setSysRegionId("cn-hangzhou");
request.setPhoneNumbers(phoneNumbers);
//签名
request.setSignName(signName);
//模板code
request.setTemplateCode(templateCode);
request.setTemplateParam("{\"code\":\""+param+"\"}");
try {
SendSmsResponse response = client.getAcsResponse(request);
System.out.println("短信发送成功");
}catch (ClientException e) {
e.printStackTrace();
}
}
}
/**
* 随机生成验证码工具类
*/
public class ValidateCodeUtils {
/**
* 随机生成验证码
* @param length 长度为4位或者6位
* @return
*/
public static Integer generateValidateCode(int length){
Integer code =null;
if(length == 4){
code = new Random().nextInt(9999);//生成随机数,最大为9999
if(code < 1000){
code = code + 1000;//保证随机数为4位数字
}
}else if(length == 6){
code = new Random().nextInt(999999);//生成随机数,最大为999999
if(code < 100000){
code = code + 100000;//保证随机数为6位数字
}
}else{
throw new RuntimeException("只能生成4位或6位数字验证码");
}
return code;
}
/**
* 随机生成指定长度字符串验证码
* @param length 长度
* @return
*/
public static String generateValidateCode4String(int length){
Random rdm = new Random();
String hash1 = Integer.toHexString(rdm.nextInt());
String capstr = hash1.substring(0, length);
return capstr;
}
}
具体业务代码实现
@PostMapping("/sendMsg")
public R<String> sendMsg(@RequestBody User user, HttpServletRequest request){
//获取手机号
String phone = user.getPhone();
if(!StringUtils.isEmpty(phone)){
//生成随机的4位验证码
String code = ValidateCodeUtils.generateValidateCode(4).toString();
//调用阿里云提供的短信服务API完成发送短信
//SMSUtils.sendMessage("","",phone,code);
//需要将生成的验证码保存到Session
request.getSession().setAttribute(phone,code);
return R.success("短信发送成功!");
}
return R.error("短信发送失败!");
}
10、邮箱验证码注册
导入依赖
<!--邮件发送-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
<version>2.6.1</version>
</dependency>
获取username 、 password

配置文件
spring:
#发送邮件配置相关
# 配置邮件服务器的地址 smtp.qq.com
mail:
host: smtp.qq.com
# 配置邮件服务器的端口(465或587)
port: 465
username: 2073958890@qq.com
password: ievhywdhgrdvcddd
default-encoding: utf-8
# SSL 连接配置
properties:
mail:
smtp:
socketFactory:
class: javax.net.ssl.SSLSocketFactory
# 开启 debug,这样方便开发者查看邮件发送日志
debug: true
注意:大坑
一定要配置SSL连接,不然代码对了也依然收不到邮件!!!
核心代码
//依赖注入
private final JavaMailSender javaMailSender;
@Transactional(rollbackFor = Exception.class)
protected void sendEmail(String toEmail,String code) throws SystemException {
try {
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage,true);
//获取配置文件中的邮箱用户
helper.setFrom(emailConfig.getUsername());
helper.setTo(toEmail);
EmailModelDto emailModelDto = redisComponent.getEmailModelDto();
helper.setSubject(emailModelDto.getEmailTitle());
helper.setText(String.format(emailModelDto.getEmailContent(),code));
helper.setSentDate(new Date());
javaMailSender.send(mimeMessage);
}catch (Exception e){
log.info("邮箱验证码发送失败!");
throw new SystemException(ResCodeEnum.EMAIL_SEND_fail);
}
}
**
* 邮箱格式配置类
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public class EmailModelDto implements Serializable {
private String emailTitle = "邮箱验证码";
private String emailContent = "您好,您的邮箱验证码是:%s,15分钟有效!";
//初始化用户使用空间,5M
private Integer initUseSpace = 5;
public String getEmailTitle() {
return emailTitle;
}
public void setEmailTitle(String emailTitle) {
this.emailTitle = emailTitle;
}
public String getEmailContent() {
return emailContent;
}
public void setEmailContent(String emailContent) {
this.emailContent = emailContent;
}
public Integer getInitUseSpace() {
return initUseSpace;
}
public void setInitUseSpace(Integer initUseSpace) {
this.initUseSpace = initUseSpace;
}
}
/**
* 初始化邮箱内容,并缓存到redis中
*/
@Component("RedisComponent")
public class RedisComponent {
@Autowired
private RedisUtils redisUtils;
public EmailModelDto getEmailModelDto(){
EmailModelDto emailModelDto = (EmailModelDto) redisUtils.get(Constants.REDIS_KEY_EMAIL_SETTING);
if(emailModelDto == null){
emailModelDto = new EmailModelDto();
//保存到redis中
redisUtils.set(Constants.REDIS_KEY_EMAIL_SETTING,emailModelDto);
}
return emailModelDto;
}
}
邮箱注册结合到了redis,将邮箱内容缓存到了redis中
导入依赖
<!--redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.6.1</version>
</dependency>
配置文件
#redis配置
redis:
host: localhost
#password:
port: 6379
database: 0
connect-timeout: 1800000
注意(坑):使用redis一定要将key 和 value 进行序列化,redis默认的序列化机制会乱码!!!
/**
* redis序列化
*/
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
//默认的Key序列化器为:JdkSerializationRedisSerializer
redisTemplate.setKeySerializer(new StringRedisSerializer()); // key序列化
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); // value序列化
redisTemplate.setConnectionFactory(connectionFactory);
return redisTemplate;
}
}
redis基本操作
/**
* 将邮箱格式及内容保存到redis中
*/
@Slf4j
@Component("RedisUtils")
public class RedisUtils<V> {
@Resource
private RedisTemplate<String,V> redisTemplate;
public V get(String key){
return key == null ? null : redisTemplate.opsForValue().get(key);
}
public boolean set(String key, V value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
}catch (Exception e){
log.info("设置redisKey:{},value:{}失败!",key,value);
return false;
}
}
public boolean setTime(String key, V value, Long time){
try {
if(time > 0){
redisTemplate.opsForValue().set(key,value,time,TimeUnit.SECONDS);
return true;
}else {
set(key,value);
}
return true;
}catch (Exception e){
log.info("设置redisKey:{},value:{}失败!",key,value);
return false;
}
}
}
总结:使用redis缓存时key 和 value要设置自己的序列化;邮箱注册时要进行SSL连接!!!
11、java生成验证码图片
工具类
package sixkey.utils;
/**
* ClassName:CreateImageCode
* Package:sixkey.utils
* Description
*
* @Author:@wenxueshi
* @Create:2023/5/28 - 10:38
* @Version:v1.0
*/
import org.springframework.web.bind.annotation.PathVariable;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.TileObserver;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Random;
/**
* 生成验证码图片
*/
public class CreateImageCode {
//图片宽带
private int width = 160;
//图片的高度
private int height = 40;
//验证码字符个数
private int codeCount = 4;
//验证码干扰线数
private int lineCount = 20;
//验证码
private String code = null;
//验证码图片Buffer
private BufferedImage bufferedImage = null;
Random random = new Random();
public CreateImageCode(){
creatImage();
}
public CreateImageCode(int width,int height){
this.width = width;
this.height = height;
creatImage();
}
public CreateImageCode(int width,int height,int codeCount){
this.width = width;
this.height = height;
this.codeCount = codeCount;
creatImage();
}
public CreateImageCode(int width,int height,int codeCount,int lineCount){
this.width = width;
this.height = height;
this.codeCount = codeCount;
this.lineCount = lineCount;
creatImage();
}
//生成图片
private void creatImage(){
//字体宽度
int fontWidth = width / codeCount;
//字体高度
int fontHeight = height -5;
int codeY = height - 8;
//图像buffer
bufferedImage = new BufferedImage(width,height,BufferedImage.TYPE_INT_RGB);
Graphics g = bufferedImage.getGraphics();
//设置背景颜色
g.setColor(getRandColor(200,250));
g.fillRect(0,0,width,height);
//设置字体
Font font = new Font("Fixedsys", Font.BOLD,fontHeight);
g.setFont(font);
//设置干扰线
for(int i = 0; i< lineCount; i++){
int xs = random.nextInt(width);
int ys = random.nextInt(height);
int xe = xs + random.nextInt(width);
int ye = ys + random.nextInt(height);
g.setColor(getRandColor(1,255));
g.drawLine(xs,ys,xe,ye);
}
//添加噪点
float yawpRate = 0.01f;
int area = (int) (yawpRate * width * height);
for(int i = 0; i < area; i++){
int x = random.nextInt(width);
int y = random.nextInt(height);
bufferedImage.setRGB(x,y,random.nextInt(255));
}
//得到随机字符
String str1 = randomStr(codeCount);
this.code = str1;
for(int i = 0; i < codeCount; i++){
String strRand = str1.substring(i,i + 1);
g.setColor(getRandColor(1,255));
g.drawString(strRand,i * fontWidth + 3, codeY);
}
}
//得到随机字符
private String randomStr(int n){
String str1 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890";
String str2 = "";
int len = str1.length() - 1;
double r;
for(int i = 0 ; i < n; i++){
r = (Math.random()) * len;
str2 = str2 + str1.charAt((int) r);
}
return str2;
}
//得到随机颜色
private Color getRandColor(int fc,int bc){
if(fc > 255) fc = 255;
if(bc > 255) bc = 255;
int r = fc + random.nextInt(bc - fc);
int g = fc + random.nextInt(bc - fc);
int b = fc + random.nextInt(bc - fc);
return new Color(r,g,b);
}
//产生随机字体
private Font getFont(int size){
Random random = new Random();
Font font[] = new Font[5];
font[0] = new Font("Ravie",Font.PLAIN,size);
font[1] = new Font("Antique Olive Compact",Font.PLAIN,size);
font[2] = new Font("Fixedsys",Font.PLAIN,size);
font[3] = new Font("Wide Latin",Font.PLAIN,size);
font[4] = new Font("Gill Sans Ultra Bold",Font.PLAIN,size);
return font[random.nextInt(5)];
}
public void write(OutputStream sos) throws IOException {
ImageIO.write(bufferedImage,"png",sos);
sos.close();
}
public BufferedImage getBuffImg(){
return bufferedImage;
}
public String getCode(){
return code.toLowerCase();
}
}
使用
CreateImageCode vCode = new CreateImageCode(130, 38, 5, 10);
//设置返回值格式
response.setHeader("Pragma", "no-cache");
response.setHeader("Cache-Control", "no-cache");
response.setDateHeader("Expires", 0);
response.setContentType("image/jpeg");
String code = vCode.getCode();
//判断登录方式
if (type == null || type == Constants.ZERO) {
//type=0是登录时发送的验证码
session.setAttribute(Constants.CHECK_CODE_KEY,code);
}else {
//前端做了默认说明:type = 1 是注册时发送的验证码
session.setAttribute(Constants.CHECK_CODE_KEY_EMAIL,code);
}
vCode.write(response.getOutputStream());
12、配置请求过滤器(Filter)
前后端未分离版
实现Filter类
/**
* 过滤器注解urlPatterns = "/*"表示过滤掉所有请求
*/
@WebFilter(filterName = "LoginCheckFilter",urlPatterns = "/*")
@Slf4j
public class LoginCheckFilter implements Filter {
//路径匹配器,支持通配符
private static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
//1.获取当前的请求url
String requestURI = request.getRequestURI();
//以下是不需要处理的请求url,直接放行
String urls[] = new String[]{
"/employee/login",
"/employee/logout",
"/backend/**",
"/front/**"
};
//2.判断本次请求是否需要处理
boolean check = check(urls, requestURI);
//3.如果不需要处理则直接放行
if(check){
log.info("本次请求不需要处理:{}",request.getRequestURI());
filterChain.doFilter(request,response);
return;
}
//4.查看登录状态,如果已登录则放行
if(request.getSession().getAttribute("employee") != null){
log.info("用户已登录,id为:"+request.getSession().getAttribute("employee"));
filterChain.doFilter(request,response);
return;
}
log.info("用户未登录!");
//5.如果未登录则返回未登录结果,通过输出流方式响应给前端
//如果未登录,只要返回NOTLOGIN字符串,页面跳转由前端js来控制
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
return;
}
/**
* 判断本次请求是否需要处理封装方法
* @param urls
* @param requestURI
* @return
*/
public boolean check(String[] urls,String requestURI){
for (String url : urls) {
//如果match为true,说明当前请求路径为不需要处理的请求,则说明路径匹配上
boolean match = PATH_MATCHER.match(url, requestURI);
if(match){
return true;
}
}
return false;
}
}
在启动类加@ServletComponentScan
@Slf4j//lombok提供的日志
@SpringBootApplication//启动类
@ServletComponentScan//开启过滤器
public class ReggieApplication {
public static void main(String[] args) {
SpringApplication.run(ReggieApplication.class,args);
log.info("项目启动成功!");
}
}
13、Mybatis-Plus分页查询
MybatisPlusInterceptorMybatisPlusInterceptor是一系列的实现InnerInterceptor的拦截器链,也可以理解为一个集合。可以包括如下的一些拦截器:
自动分页: PaginationInnerInterceptor(最常用) 多租户: TenantLineInnerInterceptor 动态表名: DynamicTableNameInnerInterceptor 乐观锁: OptimisticLockerInnerInterceptor sql性能规范: IllegalSQLInnerInterceptor 防止全表更新与删除: BlockAttackInnerInterceptor
分页插件配置类
/**
* 配置MP的分页插件
*/
@Configuration
public class MybatisPlusPageConfig {
@Bean//交给spring管理
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mybatisPlusInterceptor;
}
}
分页查询
/**
* 员工信息分页查询
* @param page
* @param pageSize
* @param name
* @return
*/
@GetMapping("/page")
public R<Page> page(int page,int pageSize,String name){
log.info("page={},pageSize={},name={}",page,pageSize,name);
//构造分页构造器
Page pageInfo = new Page(page,pageSize);
//构造条件构造器
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
//添加过滤条件(模糊查询)
queryWrapper.like(!StringUtils.isEmpty(name),Employee::getName,name);
//添加排序条件
queryWrapper.orderByDesc(Employee::getUpdateTime);
//执行查询
employeeService.page(pageInfo,queryWrapper);
return R.success(pageInfo);
}
分页前端代码
<el-pagination
class="pageList"
:page-sizes="[10, 20, 30, 40]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="counts"
:current-page.sync="page"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
></el-pagination>
new Vue({
el: '#member-app',
data() {
return {
input: '',
counts: 0,
page: 1,
pageSize: 10,
}
methods:{
handleSizeChange (val) {
this.pageSize = val
this.init()
},
handleCurrentChange (val) {
this.page = val
this.init()
}
}
14、ThreadLocal使用
使用前景
因为自动填充公共字段时需要获取当前登录用户的Id,所以引入了ThreadLocal。
在学习ThreadLocal之前,我们需要确认一个事情,就是客户端发送的每次http请求,对应的在服务端都会分配一个新的线程来处理,要确保以下所执行到的方法都是同一个线程:
1)LoginCheckFilter的doFilter方法
2)EmployeeController的update方法
3)MyMetaObjectHandler的updateFill方法
可以使用以下代码查看当前线程Id
long id = ThreadLocal.currentThread().getId();
什么是ThreadLocal?
由JDK所提供。ThreadLocal并不是一个Thread,而是Thread的局部变量,当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
ThreadLocal为每一个线程提供单独的一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。
ThreadLocal常用方法:
public void set(T value) 设置当前线程的线程局部变量的值
public T get() 返回当前线程所对应的线程局部变量的值
具体使用:
我们可以在LoginCheckFilter的doFilter方法中获取当前登录的用户Id,并调用ThreadLocal的set方法设置当前线程的线程局部变量的值 ( 用户Id ) ,然后在MyMetaObjectHandler的updateFill方法中调用ThreadLocal的get方法来获取当前的线程所对应的线程局部变量的值 ( 用户Id )。
代码实现:
/**
* 基于ThreadLocal封装工具类,用来保存和获取当前登录的用户Id
*/
public class BaseContext {
private static ThreadLocal<Long> threadLocal = new ThreadLocal<Long>();
public static void setCurrentId(Long id){
threadLocal.set(id);
}
public static Long getCurrentId(){
return threadLocal.get();
}
}
在过滤器方法里面一旦用户登录就设置用户Id
//4.查看登录状态,如果已登录则放行
if(request.getSession().getAttribute("employee") != null){
log.info("用户已登录,id为:"+request.getSession().getAttribute("employee"));
Long empId = (Long) request.getSession().getAttribute("employee");
//同一个线程保存用户Id
BaseContext.setCurrentId(empId);
filterChain.doFilter(request,response);
return;
}
15、文件上传、下载介绍
前端Form表单要求:
method="post" 采用post方式提交数据
enctype="multipart/form-data" 采用multipart格式上传文件
type="file" 使用input的file控件上传
后端代码实现:
properties文件配置:
#文件上传位置
reggie.path=D:/img/
/**
* 文件上传、下载功能
*/
@RestController
@RequestMapping("/common")
public class CommonController {
//获取配置文件中的路径
@Value("${reggie.path}")
private String basePath;
@PostMapping("/upload")
public R<String> upload(MultipartFile file) throws IOException {
//file是一个临时文件,需要转存到指定位置,否则本次请求完成后临时文件会被删除。
//获取原始文件名
String originalFilename = file.getOriginalFilename();//abc.jpg
//截取后缀.jpg
String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
//使用UUID重新生成文件名,防止上传同名文件被覆盖
String finalFileName = UUID.randomUUID().toString() + suffix;//skfjd.jpg
//创建一个目录对象
File dir = new File(basePath);
if(!dir.exists()){
//此目录不存在则创建
dir.mkdirs();
}
//将临时文件转存到指定位置
file.transferTo(new File(basePath + finalFileName));
return R.success(finalFileName);
}
}
文件下载具体实现
/**
* 文件下载
* @param name
* @param response
*/
@GetMapping("/download")
public void downLoad(String name, HttpServletResponse response){
//通过输入流读取指定文件中的File
FileInputStream fileInputStream = null;
ServletOutputStream outputStream = null;
//设置下载格式
response.setContentType("image/jpeg");
try {
fileInputStream = new FileInputStream(basePath + name);
outputStream = response.getOutputStream();
//读写文件
int len = 0;
byte[] bytes = new byte[1024];
while ((len = fileInputStream.read(bytes)) != -1 ){
outputStream.write(bytes,0,len);
outputStream.flush();
}
} catch (Exception e) {
throw new RuntimeException(e);
}
//关闭流
try {
fileInputStream.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
try {
outputStream.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
16、路径分析
1.@PathVariable使用场景
path: http://localhost:8081/page/12
2.@RequestParam使用场景
path: http://localhost:8081/page?ids=12346545
3.@RequestBody使用场景
path: http://localhost:8081/page
data{
id:12,
name:'jack',
phone:123465789
}
17、Maven聚合工程
父工程 guigu-oa-parent 依赖版本
子模块 common(公共模块)
common-util 核心工具类
service-util 业务模块工具类
子模块 model 实体类
子模块 service-oa 业务模块
项目依赖及配置
配置
#端口配置
server.port=8200
#配置数据源
spring.datasource.druid.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.druid.url=jdbc:mysql://localhost:3306/guigu-oa?serverTimezone=GMT%2B8&useSSL=false&nullCatalogMeansCurrent=true&characterEncoding=utf-8
spring.datasource.druid.username=root
spring.datasource.druid.password=020708
#配置mybatis-plus
mybatis-plus.configuration.map-underscore-to-camel-case=true
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
mybatis-plus.mapper-locations=classpath:com/guigu/mapper/xml/*.xml
#设置date格式
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8
#redis配置
spring.redis.host=82.157.234.124
spring.redis.port=6379
spring.redis.password=123456
spring.redis.database=0
spring.redis.timeout=1800000
#activiti配置
# false:默认,数据库表不变,但是如果版本不对或者缺失表会抛出异常(生产使用)
# true:表不存在,自动创建(开发使用)
# create_drop: 启动时创建,关闭时删除表(测试使用)
# drop_create: 启动时删除表,在创建表 (不需要手动关闭引擎)
spring.activiti.database-schema-update=true
#监测历史表是否存在,activities7默认不开启历史表
spring.activiti.db-history-used=true
#none:不保存任何历史数据,流程中这是最高效的
#activity:只保存流程实例和流程行为
#audit:除了activity,还保存全部的流程任务以及其属性,audit为history默认值
#full:除了audit、还保存其他全部流程相关的细节数据,包括一些流程参数
spring.activiti.history-level=full
#校验流程文件,默认校验resources下的process 文件夹的流程文件
spring.activiti.check-process-definitions=true
#微信推送配置
wechat.mpAppId=wx88a3b565c0b59c37
wechat.mpAppSecret=6ebce2277427da473f269d309f880874
# 授权回调获取用户信息接口地址
wechat.userInfoUrl=http://sixkey3.viphk.91tunnel.com/admin/wechat/userInfo
父项目pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.guigu</groupId>
<artifactId>guigu-oa-parent</artifactId>
<!--打包方式-->
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<modules>
<module>common</module>
<module>model</module>
<module>service-oa</module>
</modules>
<!--父依赖-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.6.RELEASE</version>
</parent>
<!--版本控制-->
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<java.version>1.8</java.version>
<mybatis-plus.version>3.4.1</mybatis-plus.version>
<mysql.version>8.0.30</mysql.version>
<knife4j.version>3.0.3</knife4j.version>
<jwt.version>0.9.1</jwt.version>
<fastjson.version>2.0.21</fastjson.version>
</properties>
<!--配置dependencyManagement锁定依赖的版本-->
<dependencyManagement>
<dependencies>
<!--mybatis-plus 持久层-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<!--knife4j-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>${knife4j.version}</version>
</dependency>
<!--jjwt-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jwt.version}</version>
</dependency>
<!--fastjson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.19.1</version>
<configuration>
<skipTests>true</skipTests> <!--默认关掉单元测试 -->
</configuration>
</plugin>
</plugins>
</build>
</project>
common pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>guigu-oa-parent</artifactId>
<groupId>org.guigu</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.guigu</groupId>
<artifactId>common</artifactId>
<!--打包方式-->
<packaging>pom</packaging>
<modules>
<module>service-util</module>
<module>common-util</module>
</modules>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
</project>
工具类common-util pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>common</artifactId>
<groupId>com.guigu</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.atguigu</groupId>
<artifactId>common-util</artifactId>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<!--<scope>provided </scope>-->
</dependency>
<!--jwt依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
</dependencies>
</project>
工具类service-util pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>common</artifactId>
<groupId>com.guigu</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>service-util</artifactId>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>com.atguigu</groupId>
<artifactId>common-util</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.guigu</groupId>
<artifactId>model</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- Spring Security依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--redis依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--接口文档生成依赖-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
</dependency>
</dependencies>
</project>
实体类model pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>guigu-oa-parent</artifactId>
<groupId>org.guigu</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.guigu</groupId>
<artifactId>model</artifactId>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<!--lombok用来简化实体类-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<scope>provided </scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<scope>provided </scope>
</dependency>
</dependencies>
</project>
启动类service-oa pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>guigu-oa-parent</artifactId>
<groupId>org.guigu</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.guigu</groupId>
<artifactId>service-oa</artifactId>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>com.guigu</groupId>
<artifactId>model</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.guigu</groupId>
<artifactId>service-util</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--数据库连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.23</version>
</dependency>
<!--引入activiti的springboot启动器 -->
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-spring-boot-starter</artifactId>
<version>7.1.0.M6</version>
<exclusions>
<exclusion>
<artifactId>mybatis</artifactId>
<groupId>org.mybatis</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-mp</artifactId>
<version>4.1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<!-- 指定该Main Class为全局的唯一入口 这里是启动类的地址 -->
<mainClass>com.guigu.ServiceOaApplication</mainClass>
<layout>ZIP</layout>
</configuration>
<executions>
<execution>
<!--可以把依赖的包都打包到生成的Jar包中-->
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.yml</include>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
<resource>
<directory>src/main/resources</directory>
<includes> <include>**/*.yml</include>
<include>**/*.properties</include>
<include>**/*.xml</include>
<include>**/*.png</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>
</project>
18、随机生成验证码
package com.itheima.utils;
import java.util.Random;
/**
* 随机生成验证码工具类
*/
public class ValidateCodeUtils {
/**
* 随机生成验证码
* @param length 长度为4位或者6位
* @return
*/
public static Integer generateValidateCode(int length){
Integer code =null;
if(length == 4){
code = new Random().nextInt(9999);//生成随机数,最大为9999
if(code < 1000){
code = code + 1000;//保证随机数为4位数字
}
}else if(length == 6){
code = new Random().nextInt(999999);//生成随机数,最大为999999
if(code < 100000){
code = code + 100000;//保证随机数为6位数字
}
}else{
throw new RuntimeException("只能生成4位或6位数字验证码");
}
return code;
}
/**
* 随机生成指定长度字符串验证码
* @param length 长度
* @return
*/
public static String generateValidateCode4String(int length){
Random rdm = new Random();
String hash1 = Integer.toHexString(rdm.nextInt());
String capstr = hash1.substring(0, length);
return capstr;
}
}
19、整合RabbitMQ死信队列
7.4. 整合 springboot
7.4.1导入依赖
<dependencies>
<!--RabbitMQ 依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--swagger-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<!--RabbitMQ 测试依赖-->
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
7.4.2. 修改配置文件
spring:
rabbitmq:
host: 82.157.234.124
port: 5672
username: admin
password: 123
7.4.3. 添加 Swagger 配置类
/**
* swagger2配置类
*/
@Configuration
@EnableSwagger2
public class Swagger2Config {
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.useDefaultResponseMessages(false)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.sixkey.controller"))
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.description("RabbitMQ接口文档")
.version("v1.1.0")
.title("RabbitMQ接口文档")
.build();
}
}
7.5. 队列 TTL
7.5.1. 代码架构图
创建两个队列 QA 和 QB,两者队列 TTL 分别设置为 10S 和 40S,然后在创建一个交换机 X 和死信交 换机 Y,它们的类型都是 direct,创建一个死信队列 QD,它们的绑定关系如下:

**7.5.2. 配置文件类代码 **
/**
* TTL 队列 配置类
*/
@Configuration
public class TtlQueueConfig {
//普通交换机名称
public static final String X_EXCHANGE = "X";
//普通队列名称
public static final String QUEUE_A = "QA";
//普通队列名称
public static final String QUEUE_B = "QB";
//死信交换机名称
public static final String Y_DEAD_LETTER_EXCHANGE = "Y";
//死信队列名称
public static final String DEAD_LETTER_QUEUE = "QD";
// 声明普通交换机 xExchange
@Bean("xExchange")
public DirectExchange xExchange(){
return new DirectExchange(X_EXCHANGE);
}
// 声明死信交换机 xExchange
@Bean("yExchange")
public DirectExchange yExchange(){
return new DirectExchange(Y_DEAD_LETTER_EXCHANGE);
}
//声明普通队列 A ttl 为 10s 并绑定到对应的死信交换机
@Bean("queueA")
public Queue queueA(){
Map<String, Object> args = new HashMap<>(3);
//声明当前队列绑定的死信交换机
args.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
//声明当前队列的死信路由 key
args.put("x-dead-letter-routing-key", "YD");
//声明队列的 TTL
args.put("x-message-ttl", 10000);
return QueueBuilder.durable(QUEUE_A).withArguments(args).build();
}
// 声明队列 A 绑定 X 交换机
@Bean
public Binding queueaBindingX(@Qualifier("queueA") Queue queueA,
@Qualifier("xExchange") DirectExchange xExchange){
return BindingBuilder.bind(queueA).to(xExchange).with("XA");
}
//声明普通队列 B ttl 为 40s 并绑定到对应的死信交换机
@Bean("queueB")
public Queue queueB(){
Map<String, Object> args = new HashMap<>(3);
//声明当前队列绑定的死信交换机
args.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
//声明当前队列的死信路由 key
args.put("x-dead-letter-routing-key", "YD");
//声明队列的 TTL
args.put("x-message-ttl", 40000);
return QueueBuilder.durable(QUEUE_B).withArguments(args).build();
}
//声明队列 B 绑定 X 交换机
@Bean
public Binding queuebBindingX(@Qualifier("queueB") Queue queue1B,
@Qualifier("xExchange") DirectExchange xExchange){
return BindingBuilder.bind(queue1B).to(xExchange).with("XB");
}
//声明死信队列 QD
@Bean("queueD")
public Queue queueD(){
return new Queue(DEAD_LETTER_QUEUE);
}
//声明死信队列 QD 绑定关系
@Bean
public Binding deadLetterBindingQAD(@Qualifier("queueD") Queue queueD,
@Qualifier("yExchange") DirectExchange yExchange){
return BindingBuilder.bind(queueD).to(yExchange).with("YD");
}
}
生产者端
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("sendMsg/{message}")
public void sendMsg(@PathVariable String message){
log.info("当前时间:{},发送一条信息给两个 TTL 队列:{}", new Date(), message);
rabbitTemplate.convertAndSend("X", "XA", "消息来自 ttl 为 10S 的队列: "+message);
rabbitTemplate.convertAndSend("X", "XB", "消息来自 ttl 为 40S 的队列: "+message);
}
消费者端
/**
* 延迟队列 消费者
*/
@Slf4j
@Component
public class DeadLetterQueueConsumer {
//监听死信队列
@RabbitListener(queues = "QD")
public void receiveD(Message message, Channel channel) throws IOException {
String msg = new String(message.getBody());
log.info("当前时间:{},收到死信队列信息{}", new Date().toString(), msg);
}
}
发起一个请求 http://localhost:8080/ttl/sendMsg/嘻嘻嘻

第一条消息在 10S 后变成了死信消息,然后被消费者消费掉,第二条消息在 40S 之后变成了死信消息, 然后被消费掉,这样一个延时队列就打造完成了。
不过,如果这样使用的话,岂不是**每增加一个新的时间需求,就要新增一个队列,**这里只有 10S 和 40S 两个时间选项,如果需要一个小时后处理,那么就需要增加 TTL 为一个小时的队列,如果是预定会议室然 后提前通知这样的场景,岂不是要增加无数个队列才能满足需求?
7.6. 延时队列优化
7.6.1. 代码架构图
在这里新增了一个队列 QC,绑定关系如下,该队列不设置 TTL 时间

**7.6.2. 配置文件类代码 **
//死信队列优化,引入队列C
public static final String QUEUE_C = "QC";
//优化延迟队列,引入队列C
//声明队列 C 死信交换机
@Bean("queueC")
public Queue queueC(){
Map<String, Object> args = new HashMap<>(3);
//声明当前队列绑定的死信交换机
args.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
//声明当前队列的死信路由 key
args.put("x-dead-letter-routing-key", "YD");
//没有声明 TTL 属性
return QueueBuilder.durable(QUEUE_C).withArguments(args).build();
}
//声明队列 B 绑定 X 交换机
@Bean
public Binding queuecBindingX(@Qualifier("queueC") Queue queueC,
@Qualifier("xExchange") DirectExchange xExchange){
return BindingBuilder.bind(queueC).to(xExchange).with("XC");
}
7.6.3. 消息生产者代码
//优化延迟队列
@GetMapping("sendExpirationMsg/{message}/{ttlTime}")
public void sendMsg(@PathVariable String message,@PathVariable String ttlTime) {
rabbitTemplate.convertAndSend("X", "XC", message, correlationData ->{
//设置发送消息延迟时间
correlationData.getMessageProperties().setExpiration(ttlTime);
return correlationData;
});
log.info("当前时间:{},发送一条时长{}毫秒 TTL 信息给队列 C:{}", new Date(),ttlTime, message);
}
发起请求
延迟20s
- http://localhost:8080/ttl/sendExpirationMsg/你好 1/20000
延迟2s
- http://localhost:8080/ttl/sendExpirationMsg/你好 2/2000
效果图

缺点:需要排队
看起来似乎没什么问题,但是在最开始的时候,就介绍过如果使用在消息属性上设置 TTL 的方式,消 息可能并不会按时“死亡“,因为 **RabbitMQ 只会检查第一个消息是否过期,**如果过期则丢到死信队列, 如果第一个消息的延时时长很长,而第二个消息的延时时长很短,第二个消息并不会优先得到执行。
7.7. Rabbitmq 插件弥补延迟队列需要排队缺点
弥补缺点
上文中提到的问题,确实是一个问题,如果不能实现在消息粒度上的 TTL,并使其在设置的 TTL 时间 及时死亡,就无法设计成一个通用的延时队列。那如何解决呢,接下来我们就去解决该问题。
7.7.1. 安装延时队列插件
在官网上下载 https://www.rabbitmq.com/community-plugins.html,下载 rabbitmq_delayed_message_exchange 插件,然后解压放置到 RabbitMQ 的插件目录。 进入 RabbitMQ 的安装目录下的 plgins 目录,执行下面命令让该插件生效,然后重启 RabbitMQ
①、将下载好的插件上传到rabbitmq安装目录下
②、将插件复制到插件文件夹下
cp rabbitmq_delayed_message_exchange-3.8.0.ez /usr/lib/rabbitmq/lib/rabbitmq_server-3.8.8/plugins
③、安装插件
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
④、重启rabbitmq
systemctl restart rabbitmq–server
查看是否生效

7.7.2. 代码架构图
在这里新增了一个队列 delayed.queue,一个自定义交换机 delayed.exchange,绑定关系如下:


7.7.3. 配置文件类代码
在我们自定义的交换机中,这是一种新的交换类型,该类型消息支持延迟投递机制 消息传递后并 不会立即投递到目标队列中,而是存储在 mnesia(一个分布式数据系统)表中,当达到投递时间时,才 投递到目标队列中。
/**
* 基于插件的 延迟队列配置:队列、交换机、routingKey
*/
@Configuration
public class DelayedQueueConfig {
//延迟队列名称
public static final String DELAYED_QUEUE_NAME = "delayed.queue";
//延迟交换机名称
public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";
//延迟routingKey名称
public static final String DELAYED_ROUTING_KEY = "delayed.routingkey";
//声明延迟队列
@Bean
public Queue delayedQueue() {
return new Queue(DELAYED_QUEUE_NAME);
}
//自定义交换机 我们在这里定义的是一个延迟交换机
@Bean
public CustomExchange delayedExchange() {
Map<String, Object> args = new HashMap<>();
//自定义交换机的类型
args.put("x-delayed-type", "direct");
return new CustomExchange(DELAYED_EXCHANGE_NAME, "x-delayed-message", true, false,
args);
}
//将队列、交换机、routingKey进行绑定
@Bean
public Binding bindingDelayedQueue(@Qualifier("delayedQueue") Queue queue,
@Qualifier("delayedExchange") CustomExchange
delayedExchange) {
return
BindingBuilder.bind(queue).to(delayedExchange).with(DELAYED_ROUTING_KEY).noargs();
}
}
7.7.4. 消息生产者代码
//发送消息,基于插件的延迟队列
@GetMapping("sendDelayMsg/{message}/{delayTime}")
public void sendMsg(@PathVariable String message,@PathVariable Integer delayTime) {
//设置交换机名称,routingKey名称,发送的消息
rabbitTemplate.convertAndSend(DelayedQueueConfig.DELAYED_EXCHANGE_NAME, DelayedQueueConfig.DELAYED_ROUTING_KEY, message,
correlationData ->{
correlationData.getMessageProperties().setDelay(delayTime);
return correlationData;
});
log.info(" 当 前 时 间 : {}, 发送一条延迟 {} 毫秒的信息给队列 delayed.queue:{}", new
Date(),delayTime, message);
}
7.7.5. 消息消费者代码
/**
* 基于插件的 延迟队列 消费者
*/
@Slf4j
@Component
public class DelayedQueueConsumer {
//监听死信队列
@RabbitListener(queues = DelayedQueueConfig.DELAYED_QUEUE_NAME)
public void receiveDelayedQueue(Message message){
String msg = new String(message.getBody());
log.info("当前时间:{},收到延时队列的消息:{}", new Date(), msg);
}
}
发起请求:
延迟20s
- http://localhost:8080/ttl/sendDelayMsg/come on baby1/20000
延迟2s
- http://localhost:8080/ttl/sendDelayMsg/come on baby2/2000

解决
第二个消息被先消费掉了,符合预期
7.8. 总结
延时队列在需要延时处理的场景下非常有用,使用 RabbitMQ 来实现延时队列可以很好的利用 RabbitMQ 的特性,如:消息可靠发送、消息可靠投递、死信队列来保障消息至少被消费一次以及未被正 确处理的消息不会被丢弃。另外,通过 RabbitMQ 集群的特性,可以很好的解决单点故障问题,不会因为 单个节点挂掉导致延时队列不可用或者消息丢失。
当然,延时队列还有很多其它选择,比如利用 Java 的 DelayQueue,利用 Redis 的 zset,利用 Quartz 或者利用 kafka 的时间轮,这些方式各有特点,看需要适用的场景
20、集成阿里云OSS图片上传
1、需要一些配置,首先登录阿里云,开通对象存储oss
2、需要如下几个值:
aliyun: oss: file: endpoint: oss-xxxxx.aliyuncs.com keyid: LTAI5tGSrn4xxxxxx keysecret: sXW8K7HPD9uJxxxxxx bucketname: xxxx
以上这些值都是从阿里云获取的。
注意:创建用户后记得打开权限:

导入依赖
<!-- 阿里云oss依赖 -->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.10.2</version>
</dependency>
<!-- 日期工具栏依赖 -->
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.9.9</version>
</dependency>
配置文件
# 阿里云OSS
# 不同服务器地址不同
aliyun:
oss:
file:
endpoint: oss-xxx.aliyuncs.com
keyid: LTAI5tGSxxxxxx
keysecret: sXW8K7Hxxxxx
bucketname: sixxxx
编写一个配置类,属性注入
/**
* 阿里云oss图片上传文件配置
*/
@Data
@Component
public class OssConfig implements InitializingBean {
// 读取配置文件中的内容
@Value("${aliyun.oss.file.endpoint}")
private String endpoint;
@Value("${aliyun.oss.file.keyid}")
private String keyId;
@Value("${aliyun.oss.file.keysecret}")
private String keySecret;
@Value("${aliyun.oss.file.bucketname}")
private String bucketName;
// 在spring生命周期中 实例化->生成对象->属性填充后会进行afterPropertiesSet方法
// 定义空开常用变量
public static String END_POINT;
public static String KEY_ID;
public static String KEY_SECRET;
public static String BUCKET_NAME;
@Override
public void afterPropertiesSet() throws Exception {
END_POINT = endpoint;
KEY_ID = keyId;
KEY_SECRET = keySecret;
BUCKET_NAME = bucketName;
}
}
Controller层
/**
* 阿里云OSS图片上传
*/
@Api("阿里云OSS图片上传")
@RestController
@RequiredArgsConstructor
public class FileUploadController {
private final IFileUploadService fileUploadService;
@ApiOperation(value = "图片上传", httpMethod = "POST",response = ResResult.class, notes = "图片上传")
@PostMapping("/upload")
public ResResult uploadFile(MultipartFile file){
String url = fileUploadService.uploadFileAvatar(file);
System.out.println("========url==========" + url);
return ResResult.okResult(url);
}
@ApiOperation(value = "删除图片", httpMethod = "POST",response = ResResult.class, notes = "删除图片")
@PostMapping("/delete")
public ResResult deleteFile(@RequestParam("filePath") String filePath){
fileUploadService.deleteFile(filePath);
return ResResult.okResult();
}
}
实现层
/**
* ClassName:FileUploadServiceImpl
* Package:sixkey.service.impl
* Description
*
* @Author:@wenxueshi
* @Create:2023/6/5 - 23:59
* @Version:v1.0
*/
/**
* 阿里云OSS图片上传
*/
@Service
public class FileUploadServiceImpl implements IFileUploadService {
// 从工具类中获取对象
private String endpoint = OssConfig.END_POINT;
private String accessKeyId = OssConfig.KEY_ID;
private String accessKeySecret = OssConfig.KEY_SECRET;
private String bucketName = OssConfig.BUCKET_NAME;
// 上传头像到oss
@Override
public String uploadFileAvatar(MultipartFile file) {
try {
// 创建OSS实例
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
// 获取上传文件的输入流
InputStream inputStream = file.getInputStream();
// 获取文件原始名称
String filename = file.getOriginalFilename();
// 完善1、 --> 在文件名中添加唯一值
String uuid = UUID.randomUUID().toString().replace("-", "");
filename = uuid + filename;
// 完善2、 --> 把文件按照日期分类
String datePath = new DateTime().toString("yyyy/MM/dd");
// 拼接时间 yyyy/MM/dd/filename
filename = datePath + "/" + filename;
//System.out.println(filename);
// 调用oss方法实现上传
// 1、bucketName 2、上传到oss文件路径和文件名称 3、文件的输入流
ossClient.putObject(bucketName, filename, inputStream);
// 获取url地址(根据阿里云oss中的图片实例拼接字符串) 拼接url字符串
// https://edu-leader.oss-cn-beijing.aliyuncs.com/%E4%BB%96.jpg
String url = "https://"+bucketName+"."+endpoint+"/"+filename;
// 关闭oss
ossClient.shutdown();
return url;
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
@Override
public void deleteFile(String filePath) {
String https = "https://";
String replaceFilePath = https + bucketName + "." + endpoint + "/";
// 创建OSS实例
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
// 根据BucketName,filePath删除文件:filePath只能是文件路径,
//比如我图片路径为:2023/6/6/xxxxxxxxxxxxxxxxxxxxxxxxx.jpg , filePath = 2023/6/6/xxxxxxxxxxxxxxxxxxxxxxxxx.jpg
ossClient.deleteObject(bucketName, filePath.replace(replaceFilePath,""));
ossClient.shutdown();
}
}
特别注意
这里是通过apifox测试的
这里的参数名file一定要和Controller层的参数名file一致,类型也要是file类型,不然后端一直报null异常

完整测试

判断是否成功阿里云页面

21、权限管理
1、根据用户查询角色、查询所有角色
①、数据库设计
用户表

角色表

用户角色表

②、根据用户获取角色数据、所有角色数据
controller层
/**
* url: `${api_name}/toAssign/${adminId}`,
* method: 'get'
*/
@ApiOperation(value = "根据用户获取角色数据")
@GetMapping("/toAssign/{adminId}")
public Result toAssign(@PathVariable("adminId") Long adminId){
//map集合中包含两部分:1、所有角色列表;2、用户已有的角色列表
Map<String,Object> map = roleService.findRoleByUserId(adminId);
return Result.ok(map);
}
Impl层
@Override
public Map<String, Object> findRoleByUserId(Long adminId) {
/*
整体思路:
1、第一去角色表查询到所有角色数据。
2、第二通过用户id查询用户角色表获取此用户所对应的所有数据,因为一个用户可能同时拥有多个角色,
所以返回的类型自然是一个集合list来封装。
3、第三通过第二步查询到的用户角色数据,通过stream()流获取特定数据:角色id,因为有多个,所以封装在集合中。
4、将第一步和第三步查询到的角色id做匹配,如果匹配成功,则说明此角色已经分配给此用户,
将此角色添加到一个新的集合中(因为有多个角色)。
5、最后将所有角色和已分配给用户的角色这两部分数据封装到map中进行返回。
*/
//查询所有的角色:条件为Null
List<Role> allRoleList = baseMapper.selectList(null);
//构造查询条件:通过用户id查询用户角色关系表的所有数据
LambdaQueryWrapper<AdminRole> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(AdminRole::getAdminId,adminId);
//一个用户id可能会有多个角色id的值,所有将返回一个集合List
List<AdminRole> adminRoleList = adminRoleService.list(wrapper);
//通过查询得到的adminRoleList获取角色id
List<Long> roleList = adminRoleList.stream()
.map(item -> item.getRoleId())
.collect(Collectors.toList());
//通过查询得到的roleList查看roleList是否包含在所有角色id allRoleList中
//如果存在,此角色id就是已经为用户分配过的角色,最后封装在一个新的list中放到map中进行返回
List<Role> assignedRoleList = new ArrayList<>();
for (Role role : allRoleList) {
//判断roleList是否在allRoleList中
if(roleList.contains(role.getId())){
//如果存在,此角色id就是已经为用户分配过的角色,最后将角色封装在一个新的角色list中
assignedRoleList.add(role);
}
}
//封装到map,进行返回
Map<String,Object> map = new HashMap<>();
//所有角色
map.put("allRolesList",allRoleList);
//已分配的角色
map.put("assignRoles",assignedRoleList);
return map;
}
2、根据用户分配角色
controller层
/**
* url: `${api_name}/doAssign`,
* method: 'post',
* params: {
* adminId,
* roleId
* }
* @param adminId
* @param roleId
* @return
*/
@ApiOperation(value = "根据用户分配角色")
@PostMapping("/doAssign")
public Result doAssign(@RequestParam Long adminId,@RequestParam Long[] roleId) {
roleService.saveUserRoleRealtionShip(adminId,roleId);
return Result.ok();
}
Impl层
/**
* 根据用户分配角色
* @param adminId
* @param roleIds
*/
@Override
public void saveUserRoleRealtionShip(Long adminId, Long[] roleIds) {
//分配之前将原来在用户角色表中分配过的角色数据删除,通过用户id
LambdaQueryWrapper<AdminRole> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(AdminRole::getAdminId,adminId);
adminRoleService.remove(wrapper);
//重新分配
//遍历角色id(roleIds),得到每个角色id,拿着每个角色id + adminId添加到用户角色表中。
/*for (Long roleId : roleIds) {
AdminRole adminRole = new AdminRole();
adminRole.setRoleId(roleId);
adminRole.setAdminId(adminId);
adminRoleService.save(adminRole);
}*/
//以上方法每添加一个就要重新save一次
//优化
List<AdminRole> adminRoles = new ArrayList<>();
for (Long roleId : roleIds) {
AdminRole adminRole = new AdminRole();
adminRole.setRoleId(roleId);
adminRole.setAdminId(adminId);
adminRoles.add(adminRole);
}
//批量添加
adminRoleService.saveBatch(adminRoles);
}
3、构建菜单树
表结构
操作shequ-acl数据库中的permission表

重点字段说明:
type:菜单类型,分为:目录、菜单与按钮
目录:一个分类(可理解为一级菜单)、目录下级节点可以为目录与菜单
菜单:一个具体页面,菜单的下级节点只能是按钮
按钮:页面上的功能
to_code:对应路由里面的路由地址path
code:对应菜单的功能权限标识
4.2.2 示例数据

4.2.3 页面效果

构建菜单树返回
controller层
@ApiOperation(value = "获取菜单列表")
@GetMapping
public Result index() {
List<Permission> list = permissionService.queryAllMenu();
return Result.ok(list);
}
impl层
/**
* 获取菜单列表
* @return
*/
@Override
public List<Permission> queryAllMenu() {
//查询所有菜单
List<Permission> allPermissionList = baseMapper.selectList(null);
//获取菜单树
List<Permission> result = PermissionUtils.buildPermissions(allPermissionList);
return result;
}
构建菜单树工具类
//构建菜单树
public static List<Permission> buildPermissions(List<Permission> allPermissionList) {
//封装最终返回list集合
List<Permission> treeList = new ArrayList<>();
//遍历所有菜单,得到第一层数据(父菜单),Pid = 0
for (Permission permission : allPermissionList) {
//判断是否是父菜单
if(permission.getPid() == 0){
//设置层级为第一层
permission.setLevel(1);
//从第一层开始往下找
treeList.add(findChildren(permission,allPermissionList));
}
}
return treeList;
}
//递归找子菜单
//permission:上层节点,从这里往下面找子节点
//allPermissionList: 所有菜单数据(包括子菜单和父菜单)
private static Permission findChildren(Permission permission, List<Permission> allPermissionList) {
//设置菜单
permission.setChildren(new ArrayList<Permission>());
//遍历所有菜单
//判断当前id和Pid是否相等,封装,递归往下找
for (Permission item : allPermissionList) {
//判断当前id和Pid是否相等
if(permission.getId().longValue() == item.getPid().longValue()){
//设置层级
int level = permission.getLevel() + 1;
item.setLevel(level);
if(permission.getChildren() == null){
permission.setChildren(new ArrayList<>());
}
//封装下一层数据
permission.getChildren().add(findChildren(item,allPermissionList));
}
}
return permission;
}
递归删除菜单
cotroller层
@ApiOperation(value = "递归删除菜单")
@DeleteMapping("remove/{id}")
public Result remove(@PathVariable Long id) {
permissionService.removeChildById(id);
return Result.ok();
}
impl层
/**
* 递归删除子菜单
* @param id
*/
@Override
public void removeChildById(Long id) {
//idList中有要删除的所有菜单id
List<Long> idList = new ArrayList<>();
//根据当前id,获取到当前菜单下的所有子菜单
//如果子菜单下还有子菜单,都要获取到
//递归
this.getAllPermissionId(id,idList);
//设置当前菜单,因为当前菜单也要删除
idList.add(id);
//调用批量删除方法进行删除
baseMapper.deleteBatchIds(idList);
}
工具类
//重点:递归查找当前菜单下面的所有子菜单
//第一个参数是当前菜单id
//第二个参数最终封装list集合,包含所有菜单id
private void getAllPermissionId(Long id, List<Long> idList) {
LambdaQueryWrapper<Permission> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Permission::getPid,id);
//获取当前id下的所有子菜单
List<Permission> permissionsList = baseMapper.selectList(wrapper);
//获取当前id下的所有子菜单id封装到idList中
permissionsList.stream().forEach(item ->{
//封装菜单id到idList中
idList.add(item.getId());
//递归
this.getAllPermissionId(item.getId(),idList);
});
}
22、代码生成工具类

1、导入依赖
<dependencies>
<!--代码生成时一直报slf4j错误,最后加上这个依赖就可以用了,此包不一定要引入,除非报错-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-nop</artifactId>
<version>1.7.2</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.0</version>
</dependency>
</dependencies>
2、工具类
package com.sixKey;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
import com.baomidou.mybatisplus.generator.config.GlobalConfig;
import com.baomidou.mybatisplus.generator.config.PackageConfig;
import com.baomidou.mybatisplus.generator.config.StrategyConfig;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
public class CodeGet {
public static void main(String[] args) {
// 1、创建代码生成器
AutoGenerator mpg = new AutoGenerator();
// 2、全局配置
// 全局配置
//工程路径:D:\SpringBoot_Vue_project\sixKey-yx-parent
//common 和 generator是模块名
GlobalConfig gc = new GlobalConfig();
gc.setOutputDir("D:\\SpringBoot_Vue_project\\sixKey-yx-parent\\common\\generator"+"/src/main/java");
gc.setServiceName("%sService"); //去掉Service接口的首字母I
gc.setAuthor("sixKey");
gc.setOpen(false);
mpg.setGlobalConfig(gc);
// 3、数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl("jdbc:mysql://localhost:3306/shequ-sys?serverTimezone=GMT%2B8&useSSL=false");
dsc.setDriverName("com.mysql.cj.jdbc.Driver");
dsc.setUsername("root");
dsc.setPassword("020708");
dsc.setDbType(DbType.MYSQL);
mpg.setDataSource(dsc);
// 4、包配置
PackageConfig pc = new PackageConfig();
pc.setParent("com.sixKey");
//设置最后要在哪个包下生成这些代码,也可以不设置
//pc.setModuleName("sys"); //包名
pc.setController("controller");
pc.setService("service");
pc.setMapper("mapper");
mpg.setPackageInfo(pc);
// 5、策略配置
StrategyConfig strategy = new StrategyConfig();
//配置数据中要生成的表
strategy.setInclude("region","ware","region_ware");
strategy.setNaming(NamingStrategy.underline_to_camel);//数据库表映射到实体的命名策略
strategy.setColumnNaming(NamingStrategy.underline_to_camel);//数据库表字段映射到实体的命名策略
strategy.setEntityLombokModel(true); // lombok 模型 @Accessors(chain = true) setter链式操作
strategy.setRestControllerStyle(true); //restful api风格控制器
strategy.setControllerMappingHyphenStyle(true); //url中驼峰转连字符
mpg.setStrategy(strategy);
// 6、执行
mpg.execute();
System.out.println("========代码生成成功!========");
}
}
23、整合微信登录
思路

获取openId 和 密钥
配置文件
wx:
open:
# 小程序微信公众平台appId
app_id: wx15d273ce67xxxxxx
# 小程序微信公众平台api秘钥
app_secret: f7b0f0b4eb4b78dc4baxxxxx
工具类
package com.sixKey.utils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.http.Consts;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.config.RequestConfig.Builder;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLContextBuilder;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.conn.ssl.X509HostnameVerifier;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicNameValuePair;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.security.GeneralSecurityException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
public class HttpClientUtils {
public static final int connTimeout=10000;
public static final int readTimeout=10000;
public static final String charset="UTF-8";
private static HttpClient client = null;
static {
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
cm.setMaxTotal(128);
cm.setDefaultMaxPerRoute(128);
client = HttpClients.custom().setConnectionManager(cm).build();
}
public static String postParameters(String url, String parameterStr) throws ConnectTimeoutException, SocketTimeoutException, Exception{
return post(url,parameterStr,"application/x-www-form-urlencoded",charset,connTimeout,readTimeout);
}
public static String postParameters(String url, String parameterStr,String charset, Integer connTimeout, Integer readTimeout) throws ConnectTimeoutException, SocketTimeoutException, Exception{
return post(url,parameterStr,"application/x-www-form-urlencoded",charset,connTimeout,readTimeout);
}
public static String postParameters(String url, Map<String, String> params) throws ConnectTimeoutException,
SocketTimeoutException, Exception {
return postForm(url, params, null, connTimeout, readTimeout);
}
public static String postParameters(String url, Map<String, String> params, Integer connTimeout,Integer readTimeout) throws ConnectTimeoutException,
SocketTimeoutException, Exception {
return postForm(url, params, null, connTimeout, readTimeout);
}
public static String get(String url) throws Exception {
return get(url, charset, null, null);
}
public static String get(String url, String charset) throws Exception {
return get(url, charset, connTimeout, readTimeout);
}
/**
* 发送一个 Post 请求, 使用指定的字符集编码.
*
* @param url
* @param body RequestBody
* @param mimeType 例如 application/xml "application/x-www-form-urlencoded" a=1&b=2&c=3
* @param charset 编码
* @param connTimeout 建立链接超时时间,毫秒.
* @param readTimeout 响应超时时间,毫秒.
* @return ResponseBody, 使用指定的字符集编码.
* @throws ConnectTimeoutException 建立链接超时异常
* @throws SocketTimeoutException 响应超时
* @throws Exception
*/
public static String post(String url, String body, String mimeType,String charset, Integer connTimeout, Integer readTimeout)
throws ConnectTimeoutException, SocketTimeoutException, Exception {
HttpClient client = null;
HttpPost post = new HttpPost(url);
String result = "";
try {
if (StringUtils.isNotBlank(body)) {
HttpEntity entity = new StringEntity(body, ContentType.create(mimeType, charset));
post.setEntity(entity);
}
// 设置参数
Builder customReqConf = RequestConfig.custom();
if (connTimeout != null) {
customReqConf.setConnectTimeout(connTimeout);
}
if (readTimeout != null) {
customReqConf.setSocketTimeout(readTimeout);
}
post.setConfig(customReqConf.build());
HttpResponse res;
if (url.startsWith("https")) {
// 执行 Https 请求.
client = createSSLInsecureClient();
res = client.execute(post);
} else {
// 执行 Http 请求.
client = HttpClientUtils.client;
res = client.execute(post);
}
result = IOUtils.toString(res.getEntity().getContent(), charset);
} finally {
post.releaseConnection();
if (url.startsWith("https") && client != null&& client instanceof CloseableHttpClient) {
((CloseableHttpClient) client).close();
}
}
return result;
}
/**
* 提交form表单
*
* @param url
* @param params
* @param connTimeout
* @param readTimeout
* @return
* @throws ConnectTimeoutException
* @throws SocketTimeoutException
* @throws Exception
*/
public static String postForm(String url, Map<String, String> params, Map<String, String> headers, Integer connTimeout,Integer readTimeout) throws ConnectTimeoutException,
SocketTimeoutException, Exception {
HttpClient client = null;
HttpPost post = new HttpPost(url);
try {
if (params != null && !params.isEmpty()) {
List<NameValuePair> formParams = new ArrayList<NameValuePair>();
Set<Entry<String, String>> entrySet = params.entrySet();
for (Entry<String, String> entry : entrySet) {
formParams.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
}
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(formParams, Consts.UTF_8);
post.setEntity(entity);
}
if (headers != null && !headers.isEmpty()) {
for (Entry<String, String> entry : headers.entrySet()) {
post.addHeader(entry.getKey(), entry.getValue());
}
}
// 设置参数
Builder customReqConf = RequestConfig.custom();
if (connTimeout != null) {
customReqConf.setConnectTimeout(connTimeout);
}
if (readTimeout != null) {
customReqConf.setSocketTimeout(readTimeout);
}
post.setConfig(customReqConf.build());
HttpResponse res = null;
if (url.startsWith("https")) {
// 执行 Https 请求.
client = createSSLInsecureClient();
res = client.execute(post);
} else {
// 执行 Http 请求.
client = HttpClientUtils.client;
res = client.execute(post);
}
return IOUtils.toString(res.getEntity().getContent(), "UTF-8");
} finally {
post.releaseConnection();
if (url.startsWith("https") && client != null
&& client instanceof CloseableHttpClient) {
((CloseableHttpClient) client).close();
}
}
}
/**
* 发送一个 GET 请求
*
* @param url
* @param charset
* @param connTimeout 建立链接超时时间,毫秒.
* @param readTimeout 响应超时时间,毫秒.
* @return
* @throws ConnectTimeoutException 建立链接超时
* @throws SocketTimeoutException 响应超时
* @throws Exception
*/
public static String get(String url, String charset, Integer connTimeout,Integer readTimeout)
throws ConnectTimeoutException,SocketTimeoutException, Exception {
HttpClient client = null;
HttpGet get = new HttpGet(url);
String result = "";
try {
// 设置参数
Builder customReqConf = RequestConfig.custom();
if (connTimeout != null) {
customReqConf.setConnectTimeout(connTimeout);
}
if (readTimeout != null) {
customReqConf.setSocketTimeout(readTimeout);
}
get.setConfig(customReqConf.build());
HttpResponse res = null;
if (url.startsWith("https")) {
// 执行 Https 请求.
client = createSSLInsecureClient();
res = client.execute(get);
} else {
// 执行 Http 请求.
client = HttpClientUtils.client;
res = client.execute(get);
}
result = IOUtils.toString(res.getEntity().getContent(), charset);
} finally {
get.releaseConnection();
if (url.startsWith("https") && client != null && client instanceof CloseableHttpClient) {
((CloseableHttpClient) client).close();
}
}
return result;
}
/**
* 从 response 里获取 charset
*
* @param ressponse
* @return
*/
@SuppressWarnings("unused")
private static String getCharsetFromResponse(HttpResponse ressponse) {
// Content-Type:text/html; charset=GBK
if (ressponse.getEntity() != null && ressponse.getEntity().getContentType() != null && ressponse.getEntity().getContentType().getValue() != null) {
String contentType = ressponse.getEntity().getContentType().getValue();
if (contentType.contains("charset=")) {
return contentType.substring(contentType.indexOf("charset=") + 8);
}
}
return null;
}
/**
* 创建 SSL连接
* @return
* @throws GeneralSecurityException
*/
private static CloseableHttpClient createSSLInsecureClient() throws GeneralSecurityException {
try {
SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy() {
public boolean isTrusted(X509Certificate[] chain,String authType) throws CertificateException {
return true;
}
}).build();
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext, new X509HostnameVerifier() {
@Override
public boolean verify(String arg0, SSLSession arg1) {
return true;
}
@Override
public void verify(String host, SSLSocket ssl)
throws IOException {
}
@Override
public void verify(String host, X509Certificate cert)
throws SSLException {
}
@Override
public void verify(String host, String[] cns,
String[] subjectAlts) throws SSLException {
}
});
return HttpClients.custom().setSSLSocketFactory(sslsf).build();
} catch (GeneralSecurityException e) {
throw e;
}
}
}
package com.sixKey.utils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* ClassName:ConstantPropertiesUtil
* Package:com.sixKey.utils
* Description
*
* @Author:@wenxueshi
* @Create:2023/6/10 - 19:42
* @Version:v1.0
*/
/**
* 获取配置文件中的值
*/
@Component
public class ConstantPropertiesUtil implements InitializingBean {
@Value("${wx.open.app_id}")
private String appId;
@Value("${wx.open.app_secret}")
private String appSecret;
public static String WX_OPEN_APP_ID;
public static String WX_OPEN_APP_SECRET;
@Override
public void afterPropertiesSet() throws Exception {
WX_OPEN_APP_ID = appId;
WX_OPEN_APP_SECRET = appSecret;
}
}
实战
package com.sixKey.controller;
import com.alibaba.fastjson.JSONObject;
import com.sixKey.constant.RedisConst;
import com.sixKey.enums.UserType;
import com.sixKey.exception.ServiceException;
import com.sixKey.model.user.User;
import com.sixKey.result.Result;
import com.sixKey.result.ResultCodeEnum;
import com.sixKey.service.UserService;
import com.sixKey.utils.ConstantPropertiesUtil;
import com.sixKey.utils.HttpClientUtils;
import com.sixKey.utils.JwtHelper;
import com.sixKey.vo.user.LeaderAddressVo;
import com.sixKey.vo.user.UserLoginVo;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* ClassName:WeiXinApiController
* Package:com.sixKey.controller
* Description
*
* @Author:@wenxueshi
* @Create:2023/6/10 - 21:07
* @Version:v1.0
*/
@Slf4j
@RestController
@RequestMapping("/api/user/weixin")
@RequiredArgsConstructor
public class WeiXinApiController {
private final UserService userService;
private final RedisTemplate redisTemplate;
/**
* 微信授权登录借接口
* @param code
* @return
*/
@ApiOperation(value = "微信登录获取openid(小程序)")
@GetMapping("/wxLogin/{code}")
public Result callback(@PathVariable String code){
//1、得到微信返回的code临时票据值
log.info("微信返回的code临时票据值:{}",code);
if (StringUtils.isEmpty(code)) {
//如果code为空,则报非法回调请求异常
throw new ServiceException(ResultCodeEnum.ILLEGAL_CALLBACK_REQUEST_ERROR);
}
//2、拿着code + 小程序id + 小程序密钥,请求微信接口服务
//// 使用HttpClient工具请求微信接口服务
String wxOpenAppId = ConstantPropertiesUtil.WX_OPEN_APP_ID;
String wxOpenAppSecret = ConstantPropertiesUtil.WX_OPEN_APP_SECRET;
//向微信发送get请求
//拼接请求地址加参数
//%s即为占位符
StringBuffer baseAccessTokenUrl = new StringBuffer()
.append("https://api.weixin.qq.com/sns/jscode2session")
.append("?appid=%s")
.append("&secret=%s")
.append("&js_code=%s")
.append("&grant_type=authorization_code");
//给占位符设置值
String accessTokenUrl = String.format(baseAccessTokenUrl.toString(),
ConstantPropertiesUtil.WX_OPEN_APP_ID,
ConstantPropertiesUtil.WX_OPEN_APP_SECRET,
code);
//使用以上url路径通过HttpClient发送请求
String result = null;
try {
result = HttpClientUtils.get(accessTokenUrl);
}catch (Exception e){
throw new ServiceException(ResultCodeEnum.FETCH_ACCESSTOKEN_FAILD);
}
//3、请求微信接口服务后会返回两个值 session_key 和 openId
//// openId是每个微信用户的唯一标识
JSONObject resultJson = JSONObject.parseObject(result);
//如果成功,则errcode为null
if(resultJson.getString("errcode") != null){
throw new ServiceException(ResultCodeEnum.FETCH_ACCESSTOKEN_FAILD);
}
log.info("使用code换取的access_token结果 = {}",resultJson);
String accessToken = resultJson.getString("session_key");
String openId = resultJson.getString("openid");
//4、添加微信用户信息到数据库里面
//// 查询user表
//// 判断是否是第一次使用微信授权登录:如何判断?通过openId查询是否有数据可查
User user = userService.getByOpenid(openId);
//如果是第一次登录,需要设置一些值
if(null == user){
user = new User();
user.setOpenId(openId);
user.setNickName(openId);
user.setPhotoUrl("");
user.setUserType(UserType.USER);
user.setIsNew(0);
userService.save(user);
}
//5、根据userId查询提货点和团长信息
//// 提货点:查询user表得到userId,通过userId查询user_deliver表得到leaderId
//// 团长:通过leaderId查询leader表获取leader信息
LeaderAddressVo leaderAddressVo = userService.getLeaderAddressVoByUserId(user.getId());
//6、使用JWT工具根据userId和userName生成token字符串
String token = JwtHelper.createToken(user.getId(), user.getNickName());
//7、获取当前登录用户信息,放到redis中,设置有效时间
UserLoginVo userLoginVo = userService.getUserLoginVo(user.getId());
redisTemplate.opsForValue().set(RedisConst.USER_LOGIN_KEY_PREFIX + user.getId(),
userLoginVo,
RedisConst.USERKEY_TIMEOUT,
TimeUnit.DAYS);
//8、需要返回的数据封装到map中进行返回
Map<String,Object> map = new HashMap<>();
map.put("user",user);
map.put("leaderAddressVo",leaderAddressVo);
map.put("token",token);
return Result.ok(map);
}
}
24、SpringBoot整合Aop实现日志记录、权限校验、参数校验
实例1、实现日志记录
1、自定义注解
package sixkey.annotation;
import java.lang.annotation.*;
/**
* ClassName:LogAnnotation
* Package:sixkey.annotation
* Description
*
* @Author:@wenxueshi
* @Create:2023/6/22 - 10:39
* @Version:v1.0
*/
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogAnnotation {
String model() default "";
String operation() default "";
}
2、Aspect切面
package sixkey.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.context.support.HttpRequestHandlerServlet;
import sixkey.annotation.LogAnnotation;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import static org.springframework.web.context.request.RequestContextHolder.resetRequestAttributes;
/**
* ClassName:LogAspect
* Package:sixkey.aop
* Description
*
* @Author:@wenxueshi
* @Create:2023/6/22 - 10:40
* @Version:v1.0
*/
@Component
@Aspect
@Slf4j
public class LogAspect {
//切点
@Pointcut("@annotation(sixkey.annotation.LogAnnotation)")
public void pointcut(){};
//环绕通知
@Around("pointcut()")
public Object log(ProceedingJoinPoint joinPoint) throws Throwable {
//记录方法执行时间
long begin = System.currentTimeMillis();
//方法开始执行
Object result = joinPoint.proceed();
long end = System.currentTimeMillis();
long time = end - begin;
//开始记录日志
recordLog(joinPoint,time);
return result;
}
private void recordLog(ProceedingJoinPoint joinPoint,long time){
//获取方法有日志注解的方法名
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
LogAnnotation annotation = method.getAnnotation(LogAnnotation.class);
log.info("==================开始记录日志=================");
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String uri = request.getRequestURI();
String remoteAddr = request.getRemoteAddr();
String requestMethod = request.getMethod();
log.info("===============请求URI=============" + uri);
log.info("===============请求远程地址=============" + remoteAddr);
log.info("===============请求方法=============" + requestMethod);
//获取注解的参数
log.info("model:{}",annotation.model());
log.info("operation:{}",annotation.operation());
log.info("方法执行时间:" + time + "ms");
}
}
3、使用
@PostMapping("/upload")
@LogAnnotation(model="文件",operation="文件上传")
public ResResult uploadFile(MultipartFile file){
String url = fileUploadService.uploadFileAvatar(file);
return ResResult.okResult(url);
}
实例2
所有的POST请求被调用前在控制台输出被调用的提示
1、Aspect切面
package sixkey.aop;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
/**
* ClassName:PostAspect
* Package:sixkey.aop
* Description
*
* @Author:@wenxueshi
* @Create:2023/6/22 - 13:44
* @Version:v1.0
*/
@Component
@Aspect
public class PostAspect {
@Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)")
public void pointcut(){};
//前置通知,在目标方法被执行前执行
@Before("pointcut()")
public void postAdvice(){
System.out.println("post请求的advice被触发...........");
}
}
2、使用
@PostMapping("/upload")
public ResResult uploadFile(MultipartFile file){
String url = fileUploadService.uploadFileAvatar(file);
return ResResult.okResult(url);
}

实例3、参数校验、权限校验
1、自定义注解
package sixkey.annotation;
import java.lang.annotation.*;
/**
* ClassName:PermissionAnnotation
* Package:sixkey.annotation
* Description
*
* @Author:@wenxueshi
* @Create:2023/6/22 - 14:01
* @Version:v1.0
*/
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PermissionAnnotation {
String value() default "";
}
2、Aspect切面类
package sixkey.aop;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import sixkey.utils.ResCodeEnum;
import sixkey.utils.ResResult;
/**
* ClassName:PermissionAdvice
* Package:sixkey.aop
* Description
*
* @Author:@wenxueshi
* @Create:2023/6/22 - 14:11
* @Version:v1.0
*/
@Component
@Aspect
@Order(0) //数值越小优先级越高
public class PermissionAdvice {
//切点
@Pointcut("@annotation(sixkey.annotation.PermissionAnnotation)")
public void pointcut(){};
@Around("pointcut()")
public Object permissionCheckFirst(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("==========参数校验============");
//获取目标方法上的参数
Object[] args = joinPoint.getArgs();
String ids = args[0].toString();
long id = Long.parseLong(ids);
String username = args[1].toString();
System.out.println("============id值===========" + id);
System.out.println("============username值===========" + username);
//校验逻辑
if(id < 0){
return ResResult.errorResult(ResCodeEnum.USERID_ERR);
}
if(StringUtils.isEmpty(username)){
return ResResult.errorResult(ResCodeEnum.USERNAME_NOT_NULL);
}
if(!username.equals("admin")){
return ResResult.errorResult(ResCodeEnum.NOT_ADMIN);
}
return joinPoint.proceed();
}
}
3、使用
@GetMapping("/getById/{id}/{username}")
@PermissionAnnotation(value = "校验id是否合以及用户是否是管理员") //aop权限校验
public ResResult getById(@PathVariable("id") Long id,@PathVariable("username") String username){
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getId,id);
wrapper.eq(User::getUsername,username);
User user = userService.getOne(wrapper);
return ResResult.okResult(user);
}
实例4、用户操作权限校验
1、自定义注解
package sixkey.annotation;
import java.lang.annotation.*;
/**
* ClassName:UserPermissionAnnotation
* Package:sixkey.annotation
* Description
*
* @Author:@wenxueshi
* @Create:2023/6/22 - 17:28
* @Version:v1.0
*/
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface UserPermissionAnnotation {
String value() default "";
}
2、Aspect切面类
package sixkey.aop;
import lombok.RequiredArgsConstructor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import sixkey.annotation.UserPermissionAnnotation;
import sixkey.domain.entity.Permiss;
import sixkey.service.IPermissService;
import sixkey.service.IUserPermissService;
import sixkey.utils.ResCodeEnum;
import sixkey.utils.ResResult;
import java.lang.reflect.Method;
import java.util.List;
import java.util.stream.Collectors;
/**
* ClassName:UserPermissionAdvice
* Package:sixkey.aop
* Description
*
* @Author:@wenxueshi
* @Create:2023/6/22 - 17:30
* @Version:v1.0
*/
@Component
@Aspect
@Order(0)
public class UserPermissionAdvice {
@Autowired
private IPermissService permissService;
@Autowired
private IUserPermissService userPermissService;
//切点
@Pointcut("@annotation(sixkey.annotation.UserPermissionAnnotation)")
public void pointcut(){};
@Around("pointcut()")
public Object UserPermissionAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("=============用户权限校验开始===========");
Object[] args = joinPoint.getArgs();
//获取登录用户的id
String userIds = args[0].toString();
long userId = Long.parseLong(userIds);
//通过UserId查询权限用户对应的一个或多个权限id
List<Integer> permissionIdList = userPermissService.getPermissIdByUserId(userId);
//查询权限表,获取用户对应的权限
List<Permiss> permisses = permissService.listByIds(permissionIdList);
//获取到用户对应的权限的值
List<String> permissions = permisses.stream().map(item -> item.getPermission()).collect(Collectors.toList());
//获取注解对应的权限值
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
//获取方法上的注解
UserPermissionAnnotation annotation = method.getAnnotation(UserPermissionAnnotation.class);
//获取注解的内容
String value = annotation.value();
boolean flag = false;
for (String permission : permissions) {
if(permission.equals(value)){
flag = true;
}
}
//抛出无权限异常
if(!flag){
return ResResult.errorResult(ResCodeEnum.NOT_PERMISSION);
}
return joinPoint.proceed();
}
}
3、使用
package sixkey.controller;
/**
* ClassName:UserPermissionController
* Package:sixkey.controller
* Description
*
* @Author:@wenxueshi
* @Create:2023/6/22 - 17:12
* @Version:v1.0
*/
import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaCheckRole;
import cn.dev33.satoken.stp.StpUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import sixkey.Exception.UserNameExistException;
import sixkey.annotation.PermissionAnnotation;
import sixkey.annotation.UserPermissionAnnotation;
import sixkey.domain.entity.User;
import sixkey.domain.vo.UserVo;
import sixkey.service.IUserService;
import sixkey.utils.ResResult;
import javax.validation.Valid;
import java.util.List;
/**
* 用户权限校验
*/
@Api(tags = "用户权限校验")
@RestController
@RequestMapping("/user/permission")
@RequiredArgsConstructor
public class UserPermissionController {
private final IUserService userService;
//增加用户
@ApiOperation(value = "添加用户")
@PostMapping("/add/{userId}")
@UserPermissionAnnotation(value = "user:add")
public ResResult add(@PathVariable("userId") Long userId, @RequestBody User user) throws UserNameExistException {
userService.RegisterUser(user);
return ResResult.okResult();
}
//修改用户
@ApiOperation(value = "修改用户",httpMethod = "PUT",response = ResResult.class,notes = "修改用户")
@PutMapping("/update/{userId}")
@UserPermissionAnnotation(value = "user:update")
public ResResult updateUser(@PathVariable("userId") Long userId,@RequestBody User user){
userService.updateUser(user);
return ResResult.okResult();
}
//获取用户
@ApiOperation(value = "获取用户")
@GetMapping("/getById/{userId}")
@UserPermissionAnnotation(value = "user:get")
public ResResult getById(@PathVariable("userId") Long userId){
List<User> userList = userService.list();
return ResResult.okResult(userList);
}
//删除用户
@ApiOperation(value = "删除用户",httpMethod = "DELETE",response = ResResult.class,notes = "删除用户")
@DeleteMapping("/delete/{userId}/{id}")
@UserPermissionAnnotation(value = "user:delete")
public ResResult updateUser(@PathVariable("userId") Long userId,@PathVariable("id") Long id){
userService.removeById(id);
return ResResult.okResult();
}
}
25、使用selenium爬虫获取数据
下载浏览器驱动
http://chromedriver.storage.googleapis.com/index.html
版本下载浏览器版本对应的即可
将软件解压在一个无中文路径下最好。
导入依赖
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>4.10.0</version>
</dependency>
package sixkey.utils;
/**
* 爬取数据脚本
*/
@Component
public class ItScriptWorkerUtils {
public List<Content> execute(){
//设置浏览器驱动
System.setProperty("webdriver.chrome.driver","D:\\chromeDriver\\chromedriver.exe");
ChromeOptions chromeOptions = new ChromeOptions();
//默认只允许本地连接,解决403报错问题
chromeOptions.addArguments("--remote-allow-origins=*");
//初始化web测试驱动
ChromeDriver chromeDriver = new ChromeDriver(chromeOptions);
//打开入口页(要爬取数据网页的url)
String url = "http://www.itpub.net/";
chromeDriver.get(url);
try {
//线程休眠5s,确保已经打开网页
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//开始爬取数据(获取网页前十页最新消息)
int count = 1;
List<Content> contents = new ArrayList<>();
while (count <= 10){
//通过css选择器获取html
List<WebElement> elements = chromeDriver.findElements(By.cssSelector("div.right-box h4 a"));
elements.forEach(item ->{
Content content = new Content();
String title = item.getText();
String src = item.getAttribute("href");
content.setTitle(title);
content.setUrl(src);
contents.add(content);
});
//遇到下一页按钮时自动点击事件
List<WebElement> pages = chromeDriver.findElements(By.cssSelector("div.page a"));
WebElement nextPage = pages.get(pages.size() - 1);
//触发下一页按钮单机事件
nextPage.click();
count++;
try {
//线程休眠5s,确保已经下一页
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
//关闭驱动
chromeDriver.close();
return contents;
}
}
26、优雅配置接口响应和统一异常处理
Spirng 提供了 ResponseBodyAdvice 接口,支持在消息转换器执行转换之前,对接口的返回结果进行处理,再结合 @ControllerAdvice 注解即可轻松支持上述功能。
1、接口响应和统一
准备三个类
ResponseCodeEnums.java:响应类型枚举类
package sixkey.utils;
/**
* ClassName:ResponseCodeEnums
* Package:sixkey.utils
* Description
*
* @Author:@wenxueshi
* @Create:2023/7/12 - 13:20
* @Version:v1.0
*/
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
/**
* 响应类型枚举类
*/
@Getter
@AllArgsConstructor
public enum ResponseCodeEnums {
SUCCESS(200, "success"),
FAIL(500, "failed"),
HTTP_STATUS_200(200, "ok"),
HTTP_STATUS_400(400, "request error"),
HTTP_STATUS_401(401, "no authentication"),
HTTP_STATUS_403(403, "no authorities"),
HTTP_STATUS_500(500, "server error");
private final int code;
private final String message;
}
ResponseInfo.java:响应数据封装类
package sixkey.utils;
/**
* ClassName:ResponseInfo
* Package:sixkey.utils
* Description
*
* @Author:@wenxueshi
* @Create:2023/7/12 - 13:26
* @Version:v1.0
*/
import lombok.Data;
/**
* 响应类
*/
@Data
public class ResponseInfo<T> {
/**
* 状态码
*/
protected int code;
/**
* 响应信息
*/
protected String message;
/**
* 返回数据
*/
private T data;
public static <T> ResponseInfo<T> success() {
return new ResponseInfo<>();
}
public static <T> ResponseInfo<T> success(T data) {
return new ResponseInfo<>(data);
}
public static <T> ResponseInfo<T> fail(String message) {
return new ResponseInfo<>(ResponseCodeEnums.FAIL.getCode(), message);
}
public ResponseInfo() {
this.code = ResponseCodeEnums.SUCCESS.getCode();
this.message = ResponseCodeEnums.SUCCESS.getMessage();
}
public ResponseInfo(ResponseCodeEnums statusEnums) {
this.code = statusEnums.getCode();
this.message = statusEnums.getMessage();
}
/**
* 若没有数据返回,可以人为指定状态码和提示信息
*/
public ResponseInfo(int code, String msg) {
this.code = code;
this.message = msg;
}
/**
* 有数据返回时,状态码为200,默认提示信息为“操作成功!”
*/
public ResponseInfo(T data) {
this.data = data;
this.code = ResponseCodeEnums.SUCCESS.getCode();
this.message = ResponseCodeEnums.SUCCESS.getMessage();
}
/**
* 有数据返回,状态码为 200,人为指定提示信息
*/
public ResponseInfo(T data, String msg) {
this.data = data;
this.code = ResponseCodeEnums.SUCCESS.getCode();
this.message = msg;
}
}
ResponseResultHandler:响应信息增强类
package sixkey.utils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
/**
* ClassName:ResponseResultHandler
* Package:sixkey.utils
* Description
*
* @Author:@wenxueshi
* @Create:2023/7/12 - 14:21
* @Version:v1.0
*/
/**
* 响应信息增强类
*/
@Slf4j
@ControllerAdvice
public class ResponseResultHandler implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
log.info("supports:{}", returnType.getDeclaringClass().getName());
/**
* 排除swagger-ui请求返回数据增强
*/
return !returnType.getDeclaringClass().getName().contains("springfox");
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
// 如果是ResponseInfo数据类型就直接返回
if (body instanceof ResponseInfo) {
return body;
}
// 如果是空,则返回成功
else if (body == null) {
return ResponseInfo.success();
}
// 如果是异常类型就直接返回
else if (body instanceof Exception) {
return body;
}
// 如果是String类型则直接返回String类型
else if (body instanceof String) {
return body;
}
// 返回封装后的数据
else {
return ResponseInfo.success(body);
}
}
}
测试类
@RestController
@RequiredArgsConstructor
public class TestController {
private final IUserService userService;
@GetMapping("/test")
public void test(){}
//根据id获取用户
@ApiOperation(value = "根据id获取用户")
@GetMapping("/getById/{id}")
public ResponseInfo getById(@PathVariable("id") Long id){
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getId,id);
User user = userService.getOne(wrapper);
return ResponseInfo.success(user);
}
}
响应展示


2、统一异常处理
@Slf4j
@RestControllerAdvice
public class ExceptionHandlerAdvice {
/**
* 参数格式异常处理
*/
@ExceptionHandler({IllegalArgumentException.class})
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseInfo<String> badRequestException(IllegalArgumentException ex) {
log.error("参数格式不合法:{}", ex.getMessage());
return new ResponseInfo<>(HttpStatus.BAD_REQUEST.value() + "", "参数格式不符!");
}
/**
* 权限不足异常处理
*/
@ExceptionHandler({AccessDeniedException.class})
@ResponseStatus(HttpStatus.FORBIDDEN)
public ResponseInfo<String> badRequestException(AccessDeniedException ex) {
return new ResponseInfo<>(HttpStatus.FORBIDDEN.value() + "", ex.getMessage());
}
/**
* 参数缺失异常处理
*/
@ExceptionHandler({MissingServletRequestParameterException.class})
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseInfo<String> badRequestException(Exception ex) {
return new ResponseInfo<>(HttpStatus.BAD_REQUEST.value() + "", "缺少必填参数!");
}
/**
* 空指针异常
*/
@ExceptionHandler(NullPointerException.class)
@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
public ResponseInfo<String> handleTypeMismatchException(NullPointerException ex) {
log.error("空指针异常,{}", ex.getMessage());
return ResponseInfo.fail("空指针异常");
}
@ExceptionHandler(Exception.class)
@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
public ResponseInfo<String> handleUnexpectedServer(Exception ex) {
log.error("系统异常:", ex);
return ResponseInfo.fail("系统发生异常,请联系管理员");
}
/**
* 系统异常处理
*/
@ExceptionHandler(Throwable.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ResponseInfo<String> exception(Throwable throwable) {
log.error("系统异常", throwable);
return new ResponseInfo<>(HttpStatus.INTERNAL_SERVER_ERROR.value() + "系统异常,请联系管理员!");
}
}
测试
@GetMapping("/test")
public void test(){
int i = 1/0;
}
展示

自定义异常类
只需继承RuntimeException即可
/**
* 自定义异常类
*/
public class TestException extends RuntimeException{
}
在全局异常处理类中设置
@RestControllerAdvice
public class ExceptionHandlerAdvice {
@ExceptionHandler(TestException.class)
public ResponseInfo<String> exception(TestException e){
return ResponseInfo.fail("测试系统异常4");
}
}
在需要抛出异常的地方正常抛出即可
@GetMapping("/test")
public void test() throws SystemException {
int i = 1 / 1;
if(i == 1){
throw new TestException();
}
}
27、SpringBoot日志使用
1、获取日志对象
import org.slf4j.Logger; //要导这个包
@RestController
@RequiredArgsConstructor
public class TestController {
//获取日志对象(当前类的日志)
private static final Logger log = LoggerFactory.getLogger(TestController.class);
private final IUserService userService;
@GetMapping("/test")
public void test() throws SystemException {
log.trace("我是trace");
log.debug("我是debug");
log.info("我是info");
log.warn("我是warn");
log.error("我是error");
}
}
有几个注意点,第一个是日志对象Logger的包是 slf4j,Spring Boot 中内置了日志框架 Slf4j,可以让开发人员使用一致的API来调用不同的日志记录框架;第二个,一般每个类都有自己的日志文件,getLogger方法参数推荐为当前这个类,并且这个日志文件不能轻易修改,所以是private static final

可以看到只打印了三个日志,这是因为日志有级别:
日志级别的种类:
SpringBoot日志的级别用于控制输出日志的详细程度。
每种不同的日志等级对应一组不同的日志信息,级别越高,输出的日志信息就越详细。各种日志级别的含义如下:
- trace:微量,少许的意思,级别最低;
- debug:需要调试时候的关键信息打印;
- info:普通的打印信息(默认⽇志级别);
- warn:警告,不影响使用,但需要注意的问题;
- error:错误信息,级别较⾼的错误⽇志信息;
- fatal:致命的,因为代码异常导致程序退出执⾏的事件。 SpringBoot中日志级别从低到高依次为:trace < debug < info < warn < error < fatal,默认的级别是info。
日志的输出规则:当前的级别以及比当前级别高的日志才能输出(不会输出fatal),上面的 5 种不同的方法分别对应不同的日志级别,注意,没有fatal()方法,因为当出现fatal级别的时候程序就会被终止。
这也就是上面写了5个方法,只打印了3个的原因。
2、自定义日志的级别:
logging:
level:
root: error #日志等级是error ; root表示根路径
现在可以看到:只打印了error以及error以上的日志

以上这是对根路径设置级别,还可以分别对不同的目录进行同时设置。
logging:
level:
sixkey:
controller: trace #对控制层的日志设置了trace级别的日志,两者不会冲突。
可以看到打印了所有的日志

3、日志的持久化
配置文件
logging:
file:
path: D:\\logging\\
不用自己创建这个路径,会自动创建。

可以看到已经自动创建了日志文件
文件内容

它会默认在目录中创建一个文件,文件里面装了你自己写的日志内容。那么问题来了,我多次访问接口方法,它会覆盖掉这里面的内容吗?

答案是不会的,它会在后面追加。
4、配置文件名
可以进一步精确到文件名。
logging:
file:
name: D:\logging\mySpring.log

现在还有一个问题,如果程序运行时间周期很长(比如生产环境),那么日志一定是非常多的,用一个文件来存储显然不现实,那么如何让它自动地分成多个文件呢?
可以设置日志文件存储大小的最大容量。
logging:
file:
name: D:\logging\mySpring.log
logback:
rollingpolicy:
max-file-size: 1KB #设置日志大小的最大容量(一般不会这么小,这里用于演示)
这个配置项用于指定一个日志文件的最大大小,支持的单位包括 KB、MB、GB 等。,当日志文件达到指定大小后,将自动创建一个新的日志文件来继续记录日志信息,我以上面的代码为例,我经过多次请求后:

更多的属性请看官方文档常见应用程序属性(spring.io)
5、借助lombok输出日志
只需加上一个注解即可
@Slf4j
@RestController
@RequiredArgsConstructor
public class TestController {
//不再需要获取日志对象(当前类的日志)
//private static final Logger log = LoggerFactory.getLogger(TestController.class);
private final IUserService userService;
@GetMapping("/test")
public void test() throws SystemException {
log.trace("我是trace");
log.debug("我是debug");
log.info("我是info");
log.warn("我是warn");
log.error("我是error");
}
}
日志输出路径配置文件那些都不变。
28、SpringBoot自定义注解 + 异步 + 观察者模式实现日志保存
1、前期准备
导入依赖
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.16</version>
</dependency>
<!--jdbc-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
yml配置
server:
port: 8200
spring:
application:
name: swx
datasource:
url: jdbc:mysql://localhost:3306/sixkey?characterEncoding=UTF-8&useUnicode=true&useSSL=false&serverTimezone=Asia/Shanghai&tinyInt1isBit=false&allowPublicKeyRetrieval=true
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: '020708'
type: com.alibaba.druid.pool.DruidDataSource
数据库表设计,这里只为了演示,具体可自行设计
CREATE TABLE `sys_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '日志主键',
`title` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '模块标题',
`business_type` int(2) NULL DEFAULT 0 COMMENT '业务类型(0其它 1新增 2修改 3删除)',
`method` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '方法名称',
`request_method` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '请求方式',
`oper_name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '操作人员',
`oper_url` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '请求URL',
`oper_ip` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '主机地址',
`oper_time` datetime(0) NULL DEFAULT NULL COMMENT '操作时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1585197503834284034 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '操作日志记录' ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
实体类
/**
* <p>
* 操作日志记录
* </p>
*
* @author sixkey
* @since 2023-07-13
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("sys_log")
@ApiModel(value="Log对象", description="操作日志记录")
public class Log implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "日志主键")
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@ApiModelProperty(value = "模块标题")
private String title;
@ApiModelProperty(value = "业务类型(0其它 1新增 2修改 3删除)")
private Integer businessType;
@ApiModelProperty(value = "方法名称")
private String method;
@ApiModelProperty(value = "请求方式")
private String requestMethod;
@ApiModelProperty(value = "操作人员")
private String operName;
@ApiModelProperty(value = "请求URL")
private String operUrl;
@ApiModelProperty(value = "主机地址")
private String operIp;
@ApiModelProperty(value = "操作时间")
private Date operTime;
}
2、主要功能
大体思路: 先手写一个注解--->切面来进行获取要保存的数据--->一个发布者来发布要保存的数据--->一个监听者监听后保存(异步)
①、编写注解
/**
* ClassName:Log
* Package:sixkey.annotation
* Description
*
* @Author:@wenxueshi
* @Create:2023/7/13 - 1:41
* @Version:v1.0
*/
@Target(ElementType.METHOD) //注解只能用于方法上
@Retention(RetentionPolicy.RUNTIME) //修饰注解的生命周期
public @interface Log {
String value() default "";
/**
* 模块
*/
String title() default "测试模块";
/**
* 功能
*/
BusinessTypeEnum businessType() default BusinessTypeEnum.OTHER;
}
②、编写业务类型枚举类
package sixkey.Enums;
/**
* ClassName:BusinessTypeEnum
* Package:sixkey.Enums
* Description
*
* @Author:@wenxueshi
* @Create:2023/7/13 - 1:44
* @Version:v1.0
*/
/**
* 业务类型枚举
*/
public enum BusinessTypeEnum {
/**
* 其它
*/
OTHER(0,"其它"),
/**
* 新增
*/
INSERT(1,"新增"),
/**
* 修改
*/
UPDATE(2,"修改"),
/**
* 删除
*/
DELETE(3,"删除");
private Integer code;
private String message;
BusinessTypeEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
public Integer getCode() {
return code;
}
public String getMessage() {
return message;
}
}
③、编写切面
/**
* ClassName:SysLogAspect
* Package:sixkey.aop
* Description
*
* @Author:@wenxueshi
* @Create:2023/7/13 - 1:49
* @Version:v1.0
*/
@Slf4j
@Aspect
@Component
public class SysLogAspect {
@Pointcut("@annotation(sixkey.annotation.Log)")
public void pointcut(){}
@Autowired
private EventPublistener eventPublistener;
/**
* 在切点之后切入
*/
@After("pointcut()")
public void deAfter(JoinPoint joinPoint){
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
//获取方法上的注解
Log logAnnotation = method.getAnnotation(Log.class);
//获取系统基本信息
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String requestMethod = request.getMethod();
String url = request.getRequestURI();
String ip = IpUtils.getIpAddr(request);
sixkey.domain.entity.Log sysLog = new sixkey.domain.entity.Log();
sysLog.setBusinessType(logAnnotation.businessType().getCode());
sysLog.setTitle(logAnnotation.title());
sysLog.setRequestMethod(requestMethod);
sysLog.setOperIp(ip);
sysLog.setOperUrl(url);
//从登录中token获取登录人员信息即可
sysLog.setOperName("我是测试人员");
sysLog.setOperTime(LocalDateTime.now());
//发布消息
eventPublistener.publishEvent(sysLog);
log.info("========日志发送成功,内容为:{}==========",sysLog);
}
}
④、Ip工具类
/**
* ClassName:IpUtils
* Package:sixkey.utils
* Description
*
* @Author:@wenxueshi
* @Create:2023/7/13 - 2:08
* @Version:v1.0
*/
/**
* 获取系统信息工具类
*/
public class IpUtils {
/**
* 获取客户端IP
*
* @param request 请求对象
* @return IP地址
*/
public static String getIpAddr(HttpServletRequest request) {
if (request == null) {
return "unknown";
}
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Forwarded-For");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : getMultistageReverseProxyIp(ip);
}
/**
* 从多级反向代理中获得第一个非unknown IP地址
*
* @param ip 获得的IP地址
* @return 第一个非unknown IP地址
*/
public static String getMultistageReverseProxyIp(String ip) {
// 多级反向代理检测
if (ip != null && ip.indexOf(",") > 0) {
final String[] ips = ip.trim().split(",");
for (String subIp : ips) {
if (false == isUnknown(subIp)) {
ip = subIp;
break;
}
}
}
return ip;
}
/**
* 检测给定字符串是否为未知,多用于检测HTTP请求相关
*
* @param checkString 被检测的字符串
* @return 是否未知
*/
public static boolean isUnknown(String checkString) {
return StringUtils.isBlank(checkString) || "unknown".equalsIgnoreCase(checkString);
}
}
⑤、事件发布
事件发布是由ApplicationContext对象进行发布的,直接注入使用即可! 使用观察者模式的目的:为了业务逻辑之间的解耦,提高可扩展性。 这种模式在spring和springboot底层是经常出现的,大家可以去看看。 发布者只需要关注发布消息,监听者只需要监听自己需要的,不管谁发的,符合自己监听条件即可
/**
* 事件发布者
*/
@Component
public class EventPublistener {
@Autowired
private ApplicationContext applicationContext;
//发布事件
public void publishEvent(Log log){
applicationContext.publishEvent(log);
}
}
⑥、监听者
@Async:单独开启一个新线程去保存,提高效率! @EventListener:监听
/**
* 事件监听者
*/
@Slf4j
@Component
public class MyEventListener {
@Autowired
private ILogService logService;
@Async //开启线程池异步
@EventListener //开启监听
public void saveLog(Log sysLog){
log.info("=========即将异步保存到数据库中=========");
logService.save(sysLog);
}
}
⑦、设置线程池
上面会存在一个问题,并发上来后就会出现线程突然打满,导致OOM。 所以在阿里开发规范说到: 线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方 式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors 返回的线程池对象的弊端如下: 1)FixedThreadPool 和 SingleThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。 2)CachedThreadPool: 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。 3)ScheduledThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
编写自定义线程池PoolConfig :
/**
* 线程池配置
*/
@Configuration
public class PoolConfig {
@Bean
public ThreadPoolExecutor asyncExecutor(){
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5,
10,
10,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(30),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
return threadPoolExecutor;
}
}
上面就是直接使用**@Async("asyncExecutor")**就可以直接引用了!!
启动类上添加:@EnableAsync,不然@Async不会生效的!!
⑧、测试
@Log(title = "测试玩呢",businessType = BusinessTypeEnum.INSERT)
@GetMapping("/test")
public void test() throws SystemException {
log.info("测试日志保存到数据库是否成功");
}
数据库展示
