Mall开源项目业务实现分析
mall
统一结果返回
编写统一响应结果的统一返回结果的枚举类
@Getter
public enum ResultEnum {
SUCCESS(true,20000,"成功"),
UNKNOW_ERR(false,20001,"未知错误"),
PARAM_ERR(false,20002,"参数错误");
private boolean success;
private Integer code;
private String message;
ResultEnum(boolean success,Integer code,String message) {
this.success = success;
this.code = code;
this.message = message;
}
}
编写统一返回结果类
@Data
public class R{
/*
一般统一结果类包含是否成功,状态码,状态消息,响应数据
*/
private boolean success;
private Integer code;
private String message;
private Map<String,Object> data = new HashMap<>();
//设置响应成功,使用列举的枚举类
public static R ok() {
R r = new R();
r.setSuccess(ResultEnum.SUCCESS.isSuccess());
r.setCode(ResultEnum.SUCCESS.getCode());
r.setMessage(ResultEnum.SUCCESS.getMessage());
return r;
}
//设置响应失败,使用枚举列举的类
public static R err() {
R r = new R();
r.setSuccess(ResultEnum.UNKNOW_ERR.isSuccess());
r.setCode(ResultEnum.UNKNOW_ERR.getCode());
r.setMessage(ResultEnum.UNKNOW_ERR.getMessage());
return r;
}
//设置响应的数据
public R data(Map<String,Object> data) {
this.setData(data);
return this;
}
public R data(String key,Object data) {
this.data.put(key,data);
return this;
}
//设置自定义的消息,响应码,响应是否成功
public R success(boolean success) {
this.setSuccess(success);
return this;
}
public R code(Integer code) {
this.setCode(code);
return this;
}
public R message(String message) {
this.setMessage(message);
return this;
}
}
使用
/*
* 模拟数据
* */
String[] data = new String[]{"data1","data2","data3","data4"};
@RequestMapping("/success")
public R testSuccess() {
return R.ok().message("模拟数据").data("模拟数据",data);
}
@RequestMapping("/err")
public R testErr() {
return R.err();
}
springboot2.6.3整合swagger
导入依赖
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.7.0</version>
</dependency>
swagger配置
@EnableSwagger2
@Configuration
//表示只有在开发环境中有效
@Profile("dev")
public class SwaggerConfig {
@Bean
public Docket createRestApi(){
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
//为当前包下controller生成API文档
.apis(RequestHandlerSelectors.basePackage("com.chen.r.control"))
//为有@Api注解的Controller生成API文档
// .apis(RequestHandlerSelectors.withClassAnnotation(Api.class))
//为有@ApiOperation注解的方法生成API文档
// .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
//swagger标题
.title("SwaggerUI演示")
//描述
.description("mall-study")
//由谁创建
.contact("chen")
.version("1.0")
.build();
}
}
如果启动报错
spring.mvc.pathmatch.matching-strategy=ant_path_matcher
#表示mvc的路径匹配规则,ant_path_matcher表示/user/** ,/user以下的所有路径都可以被匹配到,另一种模式是正则匹配
使用
@Api(tags = "Rcontrol",description = "统一返回结果测试")
@RestController
@SuppressWarnings("all")
public class Rcontrol {
/*
* 模拟数据
* */
String[] data = new String[]{"data1","data2","data3","data4"};
@ApiOperation("成功回调")
@RequestMapping("/success")
public R testSuccess() {
return R.ok().message("模拟数据").data("模拟数据",data);
}
@ApiOperation("失败回调")
@RequestMapping("/err")
public R testErr() {
return R.err();
}
}
测试
- 访问http://localhost:8081/swagger-ui.html
springboot整合redis
导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置工具类
- 接口
public interface RedisService {
/**
* 存储数据
*/
void set(String key, String value);
/**
* 获取数据
*/
String get(String key);
/**
* 设置超期时间
*/
boolean expire(String key, long expire);
/**
* 删除数据
*/
void remove(String key);
/**
* 自增操作
* @param delta 自增步长
*/
Long increment(String key, long delta);
}
- 实现类
@Service
public class RedisServiceImpl implements RedisService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public void set(String key, String value) {
stringRedisTemplate.opsForValue().set(key, value);
}
@Override
public String get(String key) {
return stringRedisTemplate.opsForValue().get(key);
}
@Override
public boolean expire(String key, long expire) {
return stringRedisTemplate.expire(key, expire, TimeUnit.SECONDS);
}
@Override
public void remove(String key) {
stringRedisTemplate.delete(key);
}
@Override
public Long increment(String key, long delta) {
return stringRedisTemplate.opsForValue().increment(key,delta);
}
}
连接配置
#redis配置项
redis:
host: localhost # Redis服务器地址
database: 0 # Redis数据库索引(默认为0)
port: 6379 # Redis服务器连接端口
password: # Redis服务器连接密码(默认为空)
jedis:
pool:
max-active: 8 # 连接池最大连接数(使用负值表示没有限制)
max-wait: -1ms # 连接池最大阻塞等待时间(使用负值表示没有限制)
max-idle: 8 # 连接池中的最大空闲连接
min-idle: 0 # 连接池中的最小空闲连接
timeout: 3000ms # 连接超时时间(毫秒)
使用redis制作验证码发送和验证功能
public interface AuthCodeMapper {
/**
* 发送验证码
*/
R getAuthCode(String telephone);
/**
* 验证验证码
*/
R verAuthCode(String telephone,String authCode);
}
@Service
public class AuthCodeMapperImpl implements AuthCodeMapper {
@Autowired
private RedisService redisService;
@Value("${redis.key.prefix.auth}")
private String REDIS_KEY_PREFIX_AUTH;
@Value("${redis.key.expire.time}")
private Long REDIS_KEY_EXPIRE_TIME;
Logger logger = LoggerFactory.getLogger(AuthCodeMapperImpl.class);
@Override
public R getAuthCode(String telephone) {
if (!telephone.equals("null")){
Random random = new Random();
StringBuilder authCode = new StringBuilder();
for(int i=0;i<6;i++){
authCode.append(random.nextInt(10));
}
try {
redisService.set(REDIS_KEY_PREFIX_AUTH+telephone,authCode.toString());
redisService.expire(REDIS_KEY_PREFIX_AUTH+telephone,REDIS_KEY_EXPIRE_TIME);
return R.ok().data("验证码",authCode.toString());
}catch (Exception e) {
logger.error("redis连接失败");
}
}
return R.err().message("验证码获取失败");
}
@Override
public R verAuthCode(String telephone, String authCode) {
if (!(telephone.equals("null")&&authCode.equals("null"))){
String realAuthCode = redisService.get(REDIS_KEY_PREFIX_AUTH+telephone);
if (realAuthCode!=null&&realAuthCode!=""){
if (realAuthCode.equals(authCode)){
return R.ok().message("验证通过");
}
}
}
return R.err().message("验证失败");
}
}
@RestController
@Api(tags = "AuthCodeControl",description = "验证码")
public class AuthCodeControl {
@Autowired
private AuthCodeMapper authCodeMapper;
/*
* 发送验证码
* */
@ApiOperation("发送验证码")
@RequestMapping("/getAuthCode")
public R getAuthCode(@RequestParam(value = "telephone",defaultValue = "null") String telephone) {
return authCodeMapper.getAuthCode(telephone);
}
/*
* 校验验证码
* */
@ApiOperation("验证验证码")
@RequestMapping("/verAuthCode")
public R verAuthCode(@RequestParam(value = "telephone",defaultValue = "null") String telephone,
@RequestParam(value = "authCode",defaultValue = "null") String authCode) {
return authCodeMapper.verAuthCode(telephone,authCode);
}
}
springboot整合springsecurity和jwt实现认证和权限认证方案
- 代码在springsecurity中
springboot配置跨域
- 方式:
- jsoup(较老,被替换)
- cors
- CORS在服务端进行配置
- CORS需要浏览器和服务器同时支持,目前所有的浏览器都支持该功能,IE浏览器不能低于IE10
- 浏览器一旦发现AJAX请求跨域,就会自动添加一些附加的头信息,有时还会多出一次附加的请求
- 使用Nginx进行反向代理实现跨域
jsoup方式百度
cors
- 对于某个接口进行跨域
// 还可以对指定的域进行跨域 @CrossOrigin("http:localhost:8080")
@CrossOrigin
public Result getCrosResult() {
return Result.ok();
}
- 批量对接口进行跨域(
实现WebMvcConfigure接口重写即可)
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/admin/**")
.allowedOrigins("*")
.allowCredentials(true)
.allowedMethods("*")
.allowedHeaders("*")
.allowedOriginPatterns("*");
}
- 实现所有的接口跨域
@Configuration
public class GlobalCorsConfig {
@Bean
public CorsFilter corsFilter(){
CorsConfiguration config =new CorsConfiguration();
//放行哪些原始域
config.addAllowedOriginPattern("*");
//是否发送Cookie信息
config.setAllowCredentials(true);
//放行哪些原始域(请求方式)
config.addAllowedMethod("*");
//放行哪些原始域(头部信息
config.addAllowedHeader("*");
//暴露哪些头部信息(因为跨域访问默认不能获取全部头部信息)
config.addExposedHeader("*");
//2.添加映射路径
UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource();
configSource.registerCorsConfiguration("/**", config);
//3.返回新的CorsFilter.
return new CorsFilter(configSource);
}
}
- 注意点:如果是在整合了springsecurity之后还要配置security的跨域请求,否则无效
http.cors();
使用Nginx进行反向代理
对于在异常我们可以使用统一异常处理
对于Control层面的异常处理
方案一,使用注解来实现
- 自定义统一control的异常处理类,使用注解@RestControllerAdvice标注
- 注意点
- 对于以下的统一异常处理类只会处理control层面出现的异常
- 对于Filter层面出现的异常是不会捕获的
/**
* Created By CLT On 2023/6/24.
*/
@SuppressWarnings("all")
@RestControllerAdvice
//在这里@RestControllerAdvice和@ControllerAdvice@ResponseBody联合使用效果是一样的
@Component
public class GlobalException {
Logger logger = LoggerFactory.getLogger(this.getClass());
/**
* 拦截未知的运行时异常
*/
@ExceptionHandler(Exception.class)
public R handleRuntimeException(Exception e)
{
return R.err().message("未知异常,操作失败");
}
}
对于Filter中出现的异常
方案一:在Filter中直接使用请求转发到指定的不需要权限的处理的control来进行处理
httpServletRequest.getRequestDispatcher("/myerr").forward(servletRequest,servletResponse);
/**
* Created By CLT On 2023/6/25.
*/
@SuppressWarnings("all")
@RestController
public class FilterException {
//处理没有权限的
@RequestMapping("/myerr")
public R handlerException(){
return R.err().message("没有权限");
}
}
方案二:使用HandlerExceptionResolver来处理异常,可以被Control统一异常处理捕获
- 在需要的Filter中自动注入
@Autowired
@Qualifier("handlerExceptionResolver")
private HandlerExceptionResolver resolver;
- 处理异常
resolver.resolveException(httpServletRequest,httpServletResponse,null,new Exception("没有权限"));
- 对应的统一异常处理类
/**
* Created By CLT On 2023/6/24.
*/
@SuppressWarnings("all")
@RestControllerAdvice
//在这里@RestControllerAdvice和@ControllerAdvice@ResponseBody联合使用效果是一样的
@Component
public class GlobalException {
Logger logger = LoggerFactory.getLogger(this.getClass());
/**
* 拦截未知的运行时异常
*/
@ExceptionHandler(Exception.class)
public R handleRuntimeException(Exception e)
{
return R.err().message("未知异常,操作失败");
}
}
万能的工具类(hutool)
使用步骤
- 导入依赖
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.4.0</version>
</dependency>
- 开始使用
- 具体使用步骤访问
http://hutool.mydoc.io或https://www.javajike.com/book/hutool/chapter19/c1100563100e8595e8b9b126053f418c.html
Minio文件储存工具
使用步骤
- 前提已经安装了docker
- docker拉取镜像
docker pull minio/minio - linux创建minio所需的存储文件
mkdir -p /home/minio/config
mkdir -p /home/minio/data
- docker启动minio
docker run -p 9000:9000 -p 9090:9090 \
--net=host \
--name upload \
-d --restart=always \
-e "MINIO_ACCESS_KEY=minioadmin" \
-e "MINIO_SECRET_KEY=minioadmin" \
-v /home/minio/data:/data \
-v /home/minio/config:/root/.minio \
minio/minio server \
/data --console-address ":9090" -address ":9000"
- 通过外网访问
http:ip:9090
默认账号和密码都是minioadmin
- 上传的文件都会有相对应的资源访问地址
- 具体教程访问
https://www.macrozheng.com/mall/reference/minio.html#上传文件及使用
Minio结合springboot实现文件上传和删除
- 导入依赖
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>3.0.10</version>
</dependency>
- 编写相应的Control
/**
* Created By CLT On 2023/6/25.
*/
@SuppressWarnings("all")
@RestController
public class MinioControl {
Logger logger = LoggerFactory.getLogger(this.getClass());
@Value("${minio.endpoint}")
private String ENDPOINT;
@Value("${minio.bucketName}")
private String BUCKET_NAME;
@Value("${minio.accessKey}")
private String ACCESS_KEY;
@Value("${minio.secretKey}")
private String SECRET_KEY;
@ApiOperation("文件上传")
@RequestMapping(value = "/upload", method = RequestMethod.POST)
public R upload(@RequestBody MultipartFile file) {
logger.error("文件上传中");
try {
//创建一个MinIO的Java客户端
MinioClient minioClient = new MinioClient(ENDPOINT, ACCESS_KEY, SECRET_KEY);
boolean isExist = minioClient.bucketExists(BUCKET_NAME);
if (isExist) {
logger.info("存储桶已经存在!");
} else {
//创建存储桶并设置只读权限
minioClient.makeBucket(BUCKET_NAME);
minioClient.setBucketPolicy(BUCKET_NAME, "*.*", PolicyType.READ_ONLY);
}
String filename = file.getOriginalFilename();
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
// 设置存储对象名称
String objectName = sdf.format(new Date()) + "/" + filename;
// 使用putObject上传一个文件到存储桶中
minioClient.putObject(BUCKET_NAME, objectName, file.getInputStream(), file.getContentType());
logger.info("文件上传成功!");
// MinioUploadDto minioUploadDto = new MinioUploadDto();
// minioUploadDto.setName(filename);
// minioUploadDto.setUrl(ENDPOINT + "/" + BUCKET_NAME + "/" + objectName);
return R.ok().message("上传成功");
} catch (Exception e) {
logger.info("上传发生错误: {}!", e.getMessage());
}
return R.err().message("上传失败");
}
@ApiOperation("文件删除")
@RequestMapping(value = "/delete", method = RequestMethod.POST)
public R delete(@RequestParam("objectName") String objectName) {
try {
MinioClient minioClient = new MinioClient(ENDPOINT, ACCESS_KEY, SECRET_KEY);
minioClient.removeObject(BUCKET_NAME, objectName);
return R.ok().message("删除成功");
} catch (Exception e) {
e.printStackTrace();
}
return R.err().message("删除失败");
}
}
- 编写相应的配置类
# MinIO对象存储相关配置
minio:
endpoint: http://192.168.186.200:9000 #MinIO服务所在地址
bucketName: blog #存储桶名称
accessKey: minioadmin #访问的key
secretKey: minioadmin #访问的秘钥
- 注意点
- 当linux服务器和springboot应用所在系统的时差太大时候会报错
The difference between the request time and the server's time is too large.!- 在linux服务器上使用阿里的插件校准时间
yum install ntpdatentpdate ntp1.aliyun.com
- 在linux服务器上使用阿里的插件校准时间
- 当linux服务器和springboot应用所在系统的时差太大时候会报错
- 使用Jmeter进行测试
- 测试上传成功
Nginx的用处(实现静态代理和动态代理)
nginx实现静态代理
准备工作(先部署好nginx)
- 在linux上拉取nginx的镜像
docker pull nginx:1.10
- 启动nginx,拷贝配置文件
docker run -p 80:80 --name nginx \
-v /mydata/nginx/html:/usr/share/nginx/html \
-v /mydata/nginx/logs:/var/log/nginx \
-d nginx:1.10
docker container cp nginx:/etc/nginx /mydata/nginx/
- 将拷贝出来的nginx配置文件目录重命名为conf
mv /mydata/nginx/nginx /mydata/nginx/conf
- 停止并删除原来的nginx服务
docker stop nginx
docker rm nginx
- 将配置文件拷贝到宿主机上之后重新启动nginx
docker run -p 80:80 --name nginx --restart=always \
-v /mydata/nginx/html:/usr/share/nginx/html \
-v /mydata/nginx/logs:/var/log/nginx \
-v /mydata/nginx/conf:/etc/nginx \
-d nginx:1.10
- 在这里如果遇到iptable..错误,可以尝试重新启动docker
systemctl retart docker
将静态文件部署到nginx上
- 将从vue打包下来的静态文件导入到指定的nginx文件下,并将dist重新命名为blog
- 修改nginx的配置文件
server {
listen 80;
server_name www.chen.blog; #修改域名
location / {
root /usr/share/nginx/html/blog; #代理到mall文件夹中
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
- 重新启动nginx服务
docker restart nginx
- 通过局域网访问
- 注意点
- 出现了nginx部署vue项目刷新后404问题解决
server {
location / {
#在location配置中配置以下这句话就可以解决了
try_files $uri $uri/ /index.html;
}
}
- 解决请求地址的配置
server {
location /prod-api/ {
proxy_pass http://localhost:9090/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_connect_timeout 600s;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
add_header X-Cache $upstream_cache_status;
add_header Cache-Control no-cache;
expires 12h;
}
}
nginx实现动态代理
部署
安装mysql
安装redis
时间格式化
对于后台传到前台的数据进行时间格式化
- 使用
@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss",timezone ="GMT+8")注解可以解决时间格式化 - 需要注意的是@JsonFormat注解只有在实体类承载着数据并传送到前端的时候有效
- 使用springboot测试的时候是没有效果的
对于前台传过来的时间进行格式化
- 使用注解
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")来进行时间格式化 - 同样的只有实体类作为接收参数的实体类的时候才会生效
使用PageHelper进行分页
步骤
- 导入依赖
<!--非springboot项目导入的-->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.3.2</version>
</dependency>
<!--springboot项目导入的-->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.6</version>
</dependency>
- 配置分页
pagehelper:
helper-dialect: mysql #使用的数据库. mysql, oracle, sqlite
reasonable: true #开启优化,在分页页码结果没有数据的时候,会显示有数据的页码数据,也就是当当前页<1时,返回第 1 页, 当当前页 > 最大页时, 返回最后一页的数据.
support-methods-arguments: true #是否支持接口参数来传递分页参数,默认false
pageSizeZero: false #表示当 pageSize=0 时返回所有
params: count=countSql
- 使用pagehelper进行分页
@RequestMapping("/getblog")
public R getBlog(@RequestParam("page") Integer page,@RequestParam("size") Integer size){
PageHelper.startPage(page,size);
List<Blog> blogs = blogService.blogList(1, 2);
PageInfo<Blog> blogPageInfo = new PageInfo<>(blogs);
PageHelper.clearPage();
return R.ok().data("data",blogPageInfo.getList());
}
linux后台执行jar包
nohup java -jar test.jar >temp.txt &
注意点:需要等启动完后才能回车
- 在后台运行testjar包,将日志输出到temp.txt文件中,没有自动创建
使用jobs查看所有的后台运行的作业
使用fg 编号回到后台作业界面
netstat -nlp |grep :8080 :用于查看某端口占用的线程pid
springboot实现本地文件映射请求路径
- 需要在mvc中配置即可,非常nice
- 可以用来处理图片资源
@Configuration
public class GlobalCorsConfig implements WebMvcConfigurer {
/**
* 配置本地资源路径
直接将/profile/**相面的路径全部都映射为本地的磁盘路径
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/profile/**")
//后面的\\需要加上
.addResourceLocations("file:D:\\neuedu\\uploadPath\\");
}
}
使用JSR-303校验实体类参数(单个参数无效)
- 注意点:在springboot2.3之后版本不再默认继承关于jsr-303的包,需要自己导入
使用步骤
- 导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
- 实体类使用
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
@NotNull
private String username;
private String password;
}
- control使用
@RequestMapping("/vaild")
public R vaild(@Validated User user){
return R.ok().message(user.toString());
}
自定义校验
- 定义注解(需要注意的是注解中需要包含message,groups,payload)
@SuppressWarnings("all")
@Target({ElementType.FIELD,ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = CltVaildImpl.class)
public @interface CltVaild {
/**
* 校验失败的提示信息
*/
String message() default "用户名必须以\"clt\"开头";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
- 实现自定义注解的校验逻辑
//需要实现ContrainValidator<定义的注解,检验的参数类型>
public class CltVaildImpl implements ConstraintValidator<CltVaild,String> {
private String msg = null;
@Override
public void initialize(CltVaild constraintAnnotation) {
this.msg = constraintAnnotation.message();
}
/**
*
* @param s:接收被注解的对象的值
* @param constraintValidatorContext
* @return
*/
@Override
public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
//s就是被标注的属性的值
System.err.println("校验的数据是"+s);
if (s.startsWith("clt")){
return s.startsWith("clt");
}else {
constraintValidatorContext.buildConstraintViolationWithTemplate(msg).addConstraintViolation();
return false;
}
}
}
- 实体类使用
@SuppressWarnings("all")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
@CltVaild
private String username;
private String password;
}
- 使用注解
@RequestMapping("/vaild")
public R vaild(@Validated User user){
return R.ok().message(user.toString());
}
实现同一个实体类的属性在不同情况下进行校验(校验分组)
- 定义分类
public interface Type {
interface Add{};
interface Edit{};
}
- 实体类进行校验注解
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
//在Add情况下才进行校验
@CltVaild(groups = {Type.Add.class})
private String username;
@Length(min = 1,max = 6,groups = {Type.Edit.class, Type.Add.class})
private String password;
}
- 使用
@RequestMapping("/vaildUsername")
//开启Add组的校验
public R vaildUsername(@Validated(value = {Type.Add.class}) User user){
return R.ok().message(user.toString());
}
@RequestMapping("/vaildPassword")
public R vaildPassword(@Validated(value = {Type.Edit.class}) User user){
return R.ok().message(user.toString());
}
若依中的文件上传模拟
- 编写文件工具类
public class FileUtil {
public static final Logger logger = LoggerFactory.getLogger(FileUtil.class);
//实现文件上传,返回文件完整地址
public static String upload(MultipartFile file,String fileBaseUrl) throws IOException {
String datePath = new SimpleDateFormat("yyyy/MM/dd").format(new Date());
String fileName = file.getOriginalFilename();
String allPath = fileBaseUrl + File.separator + datePath + File.separator + fileName;
String backPath = "/" + datePath + "/" + fileName;
File file1 = new File(allPath);
logger.warn("上传文件地址:{}",allPath);
createDir(file1);
//上传文件
file.transferTo(Paths.get(allPath));
//返回完整的文件地址
return backPath;
}
//创建文件夹
public static void createDir(File allPath){
if (!allPath.exists()){
if (!allPath.getParentFile().exists()){
allPath.getParentFile().mkdirs();
}
}
}
}
- 接口编写
@RestController
public class UploadControl {
Logger logger = LoggerFactory.getLogger(UploadControl.class);
@Value("${file.baseUrl}")
private String fileBasePath;
@PostMapping("/fileUpload")
public R upload(@RequestParam("file") MultipartFile file) throws IOException {
logger.warn("文件名:{}\n文件大小:{}\n",file.getOriginalFilename(),(file.getSize()/1024/1024));
String backPath = FileUtil.upload(file,fileBasePath);
logger.warn("返回地址是:{}",backPath);
return R.ok().message("文件上传成功").data("filePath",backPath);
}
}
数据字典
定义
- 数据字典的功能总结概括的话,就是使系统中的各项数据变的更加的严格。这样有利于降低因为数据问题而导致的系统出现bug的问题。
利用hutool工具类实现验证码生成和验证
- 工具使用
//定义图形验证码的长、宽、验证码字符数、干扰元素个数
CircleCaptcha captcha = CaptchaUtil.createCircleCaptcha(200, 100, 4, 20);
// 生成Base64格式的图片验证码
System.out.println(captcha.getImageBase64());
// 获取本次生成的验证码
System.out.println(captcha.getCode());
// 验证用户输入验证码是否有效
System.out.println(captcha.verify("1234"));
- 生成验证码的同时也需要存储在redis中,当时通常加上自定义常量的前缀
/**
* 验证码 redis key
*/
public static final String CAPTCHA_CODE_KEY = "captcha_codes:";
// 保存验证码信息
String uuid = IdUtils.simpleUUID();
String verifyKey = Constants.CAPTCHA_CODE_KEY + uuid;
redisCache.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES);
- 登录进行验证(用户需要提交验证码和获取redis需要的uuid)
public void validateCaptcha(String username, String code, String uuid)
{
String verifyKey = Constants.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, "");
String captcha = redisCache.getCacheObject(verifyKey);
redisCache.deleteObject(verifyKey);
if (captcha == null)
{
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")));
throw new CaptchaExpireException();
}
if (!code.equalsIgnoreCase(captcha))
{
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));
throw new CaptchaException();
}
}
实体类中的serialVersionUID属性
基本介绍
-
依靠这个属性进行正确的序列化和反序列化
-
具体:https://zhuanlan.zhihu.com/p/347246506#:~:text=简单概括而言,,serialVersionUID是用于在序列化和反序列化过程中进行核验的一个版本号。 序列化运行时将一个版本号(称为serialVersionUID)与每个可序列化类相关联,该版本号在反序列化期间用于验证序列化对象的发送方和接收方是否为该对象加载了与序列化兼容的类。
JavaScriptBlob类型
- 详见
https://zhuanlan.zhihu.com/p/500199997
关于导出办公文件(csv,xlsx等)
使用apache的工具
后端实现将实体类集合数据转换成csv文件
- 所需依赖
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.12</version>
</dependency>
<!--操作excel的依赖-->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.2</version>
</dependency>
- 将实体类集合数据转换为csv文件
- 详情
hutool官网
- 详情
List<Blog> blogs = blogService.blogList(null, null, new Blog());
ExcelWriter writer = ExcelUtil.getWriter();
// 设置标题
writer.merge(8,"博客信息表");
writer.write(blogs,true);
// 将文件写入到响应流中
writer.flush(response.getOutputStream(),true);
writer.close();
IoUtil.close(out);
- control实现
// 获取所有的blog信息并且将数据到处csv文件
@RequestMapping("/export")
public void export(HttpServletResponse response) throws IOException {
// 设置响应
//response为HttpServletResponse对象
response.setContentType("application/vnd.ms-excel;charset=utf-8");
//test.xls是弹出下载对话框的文件名,不能为中文,中文请自行编码
response.setHeader("Content-Disposition","attachment;filename=test.csv");
response.setCharacterEncoding("UTF-8");
ServletOutputStream out=response.getOutputStream();
List<Blog> blogs = blogService.blogList(null, null, new Blog());
ExcelWriter writer = ExcelUtil.getWriter();
// 设置标题
writer.merge(8,"博客信息表");
writer.write(blogs,true);
// 将文件写入到响应流中
writer.flush(response.getOutputStream(),true);
writer.close();
IoUtil.close(out);
}
前端实现从响应流中获取并保存成文件
- 思路
- 将流转换为Blob二进制流对象
- 使用第三方前端文件保存方案file-saver的saveAs()实现下载保存
- 设置请求参数
export function export1(){
return request({
url: '/export',
method: 'get',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
// 一定要写需要的返回类型,不然乱码
responseType: 'blob'
})
}
- 保存文件
- 关于file-saver介绍详情
https://juejin.cn/post/6901790184841412622
- 关于file-saver介绍详情
async export1(){
let date = await export1()
let date1 = new Blob([date.data])
saveAs(date1,`test_${new Date().getTime()}.csv`)
},
使用alibaba的工具类
- easyExcel
不会出现内存溢出问题效率更高
导入依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.1.3</version>
</dependency>
导入与导出工具类
public class ExcelUtil {
/**
* 导出excel
*
* @param response 响应流
* @param type 导出类型
* @param fileName 导出文件名
* @param sheetName
* @param data 导出的数据集合
* @param <T>
*/
public static <T> void export(HttpServletResponse response, Class<T> type, String fileName, String sheetName, List<T> data) throws IOException {
EasyExcel.write(response.getOutputStream(), type)
.autoCloseStream(false)
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
.sheet(sheetName)
.doWrite(data);
response.addHeader("Content-Disposition", StrUtil.format("{}={}", "attachment;filename", URLEncoder.encode(fileName, "UTF-8")));
response.setContentType("application/vnd.ms-excel;charset=UTF-8");
}
/**
* 导入
*
* @param file 文件
* @param type 类型
* @param <T>
* @return
* @throws IOException
*/
public static <T> List<T> include(MultipartFile file, Class<T> type) throws IOException {
return EasyExcel.read(file.getInputStream(), type, null)
.autoCloseStream(false)
.doReadAllSync();
}
}
导出
- 后端
@ApiOperation(value = "导出日志", notes = "导出日志")
@GetMapping("/export")
public void export(HttpServletResponse response) throws DtoNullException, IOException {
DataResult<LogVo> logVoDataResult = logService.queryLog(new LogQuery());
List<LogVo> dataList = (List<LogVo>) logVoDataResult.getResult();
List<LogExcel> logExcelList = LogConverter.voToExcel(dataList);
ExcelUtil.export(response, LogExcel.class, "日志信息.xls", "日志列表", logExcelList);
}
- 前端使用
file-saver插件下载
导入
- 后端
@ApiOperation(value = "导入日志", notes = "导入日志")
@PostMapping("/import")
public R include(@RequestParam("file") MultipartFile file) throws IOException {
List<LogExcel> dataList = ExcelUtil.include(file, LogExcel.class);
boolean isSuccess = logService.include(dataList);
return isSuccess ? R.success() : R.faild(null, null);
}
- 前端使用
elementui组件上传
excel实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Accessors(chain = false)
@ApiModel("日志导出")
public class LogExcel implements Serializable {
private static final long serialVersionUID = 1L;
/**
* id
*/
@ExcelProperty("编号")
private Long id;
/**
* 操作模块
*/
@ExcelProperty("操作模块")
private String logSystem;
/**
* 操作类型(1表示CRUD)
*/
@ExcelProperty("操作类型")
private Integer logOptionType;
/**
* 操作人员
*/
@ExcelProperty("操作人员")
private String logOptionPerson;
/**
* 操作地址
*/
@ExcelProperty("操作地址")
private String logOptionAddressIp;
/**
* 操作地点
*/
@ExcelProperty("操作地点")
private String logOptionAddressWhere;
/**
* 操作状态(1表示成功0表示失败)
*/
@ExcelProperty(value = "操作状态", converter = DictConverter.class)
private Integer logOptionStatus;
/**
* 操作时间
*/
@ExcelProperty("操作时间")
private Date logOptionDate;
/**
* 耗费时间
*/
@ExcelProperty("耗费时间")
private Long logOptionTime;
/**
* 操作名称
*/
@ExcelProperty("操作名称")
private String logOptionName;
}
自定义转换器
- 实现
Converter接口即可
public class DictConverter implements Converter<Object> {
/**
* 在读取指定单元格时候触发
*
* @param context
* @return
* @throws Exception
*/
@Override
public Object convertToJavaData(ReadConverterContext<?> context) throws Exception {
if (context.getReadCellData().getStringValue() == null) {
return null;
}
if (context.getReadCellData().getStringValue().equals("成功")) {
return 1;
} else {
return 0;
}
}
/**
* 在读取指定单元格时候触发
*
* @param context
* @return
* @throws Exception
*/
@Override
public WriteCellData<?> convertToExcelData(WriteConverterContext<Object> context) throws Exception {
if (context.getValue() == null) {
return new WriteCellData<>("");
}
if ((Integer) context.getValue() == 0) {
return new WriteCellData<>("失败");
} else {
return new WriteCellData<>("成功");
}
}
}
别人如何实现一个业务的CRUD
对于dao层的CRUDsql的编写
- mapper接口的编写
public interface SysConfigMapper
{
/**
* 查询参数配置信息
*
* @param config 参数配置信息
* @return 参数配置信息
*/
public SysConfig selectConfig(SysConfig config);
/**
* 查询参数配置列表
*
* @param config 参数配置信息
* @return 参数配置集合
*/
public List<SysConfig> selectConfigList(SysConfig config);
/**
* 根据键名查询参数配置信息
*
* @param configKey 参数键名
* @return 参数配置信息
*/
public SysConfig checkConfigKeyUnique(String configKey);
/**
* 新增参数配置
*
* @param config 参数配置信息
* @return 结果
*/
public int insertConfig(SysConfig config);
/**
* 修改参数配置
*
* @param config 参数配置信息
* @return 结果
*/
public int updateConfig(SysConfig config);
/**
* 删除参数配置
*
* @param configId 参数ID
* @return 结果
*/
public int deleteConfigById(Long configId);
/**
* 批量删除参数信息
*
* @param configIds 需要删除的参数ID
* @return 结果
*/
public int deleteConfigByIds(Long[] configIds);
}
- 对于mapper的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="com.neuedu.system.mapper.SysConfigMapper">
<resultMap type="SysConfig" id="SysConfigResult">
<id property="configId" column="config_id" />
<result property="configName" column="config_name" />
<result property="configKey" column="config_key" />
<result property="configValue" column="config_value" />
<result property="configType" column="config_type" />
<result property="createBy" column="create_by" />
<result property="createTime" column="create_time" />
<result property="updateBy" column="update_by" />
<result property="updateTime" column="update_time" />
</resultMap>
<sql id="selectConfigVo">
select config_id, config_name, config_key, config_value, config_type, create_by, create_time, update_by, update_time, remark
from sys_config
</sql>
<!-- 查询条件 -->
<sql id="sqlwhereSearch">
<where>
<if test="configId !=null">
and config_id = #{configId}
</if>
<if test="configKey !=null and configKey != ''">
and config_key = #{configKey}
</if>
</where>
</sql>
<!--查询实现-->
<select id="selectConfig" parameterType="SysConfig" resultMap="SysConfigResult">
<include refid="selectConfigVo"/>
<include refid="sqlwhereSearch"/>
</select>
<select id="selectConfigList" parameterType="SysConfig" resultMap="SysConfigResult">
<include refid="selectConfigVo"/>
<where>
<if test="configName != null and configName != ''">
AND config_name like concat('%', #{configName}, '%')
</if>
<if test="configType != null and configType != ''">
AND config_type = #{configType}
</if>
<if test="configKey != null and configKey != ''">
AND config_key like concat('%', #{configKey}, '%')
</if>
<if test="params.beginTime != null and params.beginTime != ''"><!-- 开始时间检索 -->
and date_format(create_time,'%y%m%d') >= date_format(#{params.beginTime},'%y%m%d')
</if>
<if test="params.endTime != null and params.endTime != ''"><!-- 结束时间检索 -->
and date_format(create_time,'%y%m%d') <= date_format(#{params.endTime},'%y%m%d')
</if>
</where>
</select>
<select id="checkConfigKeyUnique" parameterType="String" resultMap="SysConfigResult">
<include refid="selectConfigVo"/>
where config_key = #{configKey} limit 1
</select>
<!--增加实现-->
<insert id="insertConfig" parameterType="SysConfig">
insert into sys_config (
<if test="configName != null and configName != '' ">config_name,</if>
<if test="configKey != null and configKey != '' ">config_key,</if>
<if test="configValue != null and configValue != '' ">config_value,</if>
<if test="configType != null and configType != '' ">config_type,</if>
<if test="createBy != null and createBy != ''">create_by,</if>
<if test="remark != null and remark != ''">remark,</if>
create_time
)values(
<if test="configName != null and configName != ''">#{configName},</if>
<if test="configKey != null and configKey != ''">#{configKey},</if>
<if test="configValue != null and configValue != ''">#{configValue},</if>
<if test="configType != null and configType != ''">#{configType},</if>
<if test="createBy != null and createBy != ''">#{createBy},</if>
<if test="remark != null and remark != ''">#{remark},</if>
sysdate()
)
</insert>
<!--更新实现-->
<update id="updateConfig" parameterType="SysConfig">
update sys_config
<set>
<if test="configName != null and configName != ''">config_name = #{configName},</if>
<if test="configKey != null and configKey != ''">config_key = #{configKey},</if>
<if test="configValue != null and configValue != ''">config_value = #{configValue},</if>
<if test="configType != null and configType != ''">config_type = #{configType},</if>
<if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
<if test="remark != null">remark = #{remark},</if>
update_time = sysdate()
</set>
where config_id = #{configId}
</update>
<!--删除实现-->
<delete id="deleteConfigById" parameterType="Long">
delete from sys_config where config_id = #{configId}
</delete>
<delete id="deleteConfigByIds" parameterType="Long">
delete from sys_config where config_id in
<foreach item="configId" collection="array" open="(" separator="," close=")">
#{configId}
</foreach>
</delete>
</mapper>
- 实体类的编写及对应关系
datetime对应Dateint对应Longvarchar对应Stringchar对应String
如何将第三方jar交给maven管理
mvn install:install-file -Dfile=E:\maven\mvn-resp\com\aspose\aspose-words\15.8.0\aspose-words-15.8.0-jdk16.jar -DgroupId=aspose -DartifactId=aspose-words -Dversion=15.8.0 -Dpackaging=jar -DgeneratePom=true
- -Dfile:是第三方jar位置
mysql数据的导入与导出
# mysql的导出
mysqldump -uroot -p密码 test > test.sql
# mysql的导入
source /home/luyi/wwhm_pms.sql
数据库标的设计
- mysql常见的数据类型
- tinyint,int,bigint:整数类型
- float,double,decimal:浮点类型
- date,dateTime,time,timestamp:时间类型
- char,varchar,text,tinytext,longtext,json:文本类型
char=>String/Byte
varchar=>String
tinyint(1)=>Boolean/Integer
int=>Integer
bigint=>Long
float=>Float
double=>Double
integer=>Integer
decimal=>BigDecimal
text=>String
datetime=>Date
timestamp=>Date/LocalDateTime
- 注意点
- char可以用数字来表示特殊意义的值
- tinyint表示布尔值0表示是1表示否
使用stream进行分页
long pageNo = dto.getPageNo();
long pageSize = dto.getPageSize();
List<EmployeeListVo> streamPage = result.stream().skip((pageNo - 1) * pageSize).limit(pageSize).collect(Collectors.toList());
long total = result.size();
long pages = (total % pageSize) != 0 ? ((total / pageSize) + 1) : (total / pageSize);
boolean hasNextPage = (total > pageNo * pageSize);
window解决端口占用问题
# 寻找指定端口号的进程号
netstat -ano | findstr "5002"
# 杀死进程
taskkill /pid 进程号 /f
使用nginx返代理github
centos安装java1.8
- 在线下载jdk
wget --no-check-certificate --no-cookies --header "Cookie: oraclelicense=accept-securebackup-cookie" http://download.oracle.com/otn-pub/java/jdk/8u131-b11/d54c1d3a095b4ff2b6607d096fa80163/jdk-8u131-linux-x64.rpm
maven打包最小技巧
- 在maven中配置打包配置
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<excludes>
<exclude>*.properties</exclude>
<exclude>*.yml</exclude>
</excludes>
<archive>
<manifest>
<!-- 为依赖包添加路径, 这些路径会写在MANIFEST文件的Class-Path下 -->
<mainClass>com.roadwhite.ly.Application</mainClass>
<addClasspath>true</addClasspath>
<classpathPrefix>lib/</classpathPrefix>
<!-- 打包时 MANIFEST.MF文件不记录的时间戳版本 -->
<useUniqueVersions>false</useUniqueVersions>
</manifest>
<manifestEntries>
<!-- 在Class-Path下添加配置文件的路径 -->
<Class-Path>conf/</Class-Path>
</manifestEntries>
</archive>
</configuration>
</plugin>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<executions>
<execution>
<id>copy-xmls</id>
<phase>process-sources</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${basedir}/target/conf</outputDirectory>
<resources>
<resource>
<directory>${basedir}/src/main/resources</directory>
<includes>
<include>*.properties</include>
<include>*.yml</include>
</includes>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>target/lib/</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
- 打包下来会有这几个目录
lib:项目依赖的所有的jar包单独打出来conf:项目的配置文件xxx.jar:jar文件
- 好处:缩短打包时间
使用easyexcel操作xlsx文件
- poi,easyexcel(poi占内存,easyexcel较好)
mybatissql中解决<=报错
第一种写法:
原符号 < <= > >= & ' "
替换符号 < <= > >= & ' "
例如:sql如下:
id >= #{startId} and id <= #{endId}
第二种写法:
大于等于
<![CDATA[ >= ]]>
小于等于
<![CDATA[ <= ]]>
例如:sql如下:
id <![CDATA[ >= ]]> #{startId} and id <![CDATA[ <= ]]> #{endId}
在sql中如何比较日期区间
- 参数类型为Date或则其他
<where>
<if test="dateBegin != null">
and date_format(create_time, '%y%m%d') >= date_format(#{dateBegin}, '%y%m%d')
</if>
<if test="dateEnd != null">
and date_format(create_time, '%y%m%d') <= date_format(#{dateEnd}, '%y%m%d')
</if>
</where>
- 关于date_format的格式化
- '%y%m%d':21/01/02
- '%Y-%m-%d':2021-01-02
idea中的断点调试
springboot整合websocket(方式1)
- 实现实时通信
步骤
- 添加依赖
- 注意点:这里如果使用
spring-boot-starter-websocket会报错,暂时没有解决
- 注意点:这里如果使用
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
</dependency>
- 配置websocket
@Configuration
@EnableWebSocket
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
- 编写连接接口
@Slf4j
@Component
@ServerEndpoint("/websocket/{username}")
public class WebSocketController {
private static final Map<String, Session> sessionMap = new ConcurrentHashMap<>();
@OnOpen
public void open(Session session, @PathParam("username") String userName) {
log.info("{}上线了", userName);
log.info("当前对话:{}", session);
sessionMap.put(session.getId(), session);
}
@OnClose
public void close(Session session) {}
@OnError
public void error(Throwable error, Session session) {}
@OnMessage
public void mesg(Session session, String message) {
// 接受到消息转发到其他客户端
log.info("{} 对话:{}", session.getId() ,message);
sendToAll(message);
}
public void sendToAll(String msg) {
Set<String> keySet = sessionMap.keySet();
for (String key : keySet) {
Session session = sessionMap.get(key);
session.getAsyncRemote().sendText(msg);
}
}
}
- 客户端
const send = document.getElementById("send");
const close = document.getElementById("close");
const msg = document.querySelector("#msg");
const dialog = document.getElementById("dialog");
// 客户端创建websocket连接实例
const webSocket = new WebSocket("ws://localhost:8081/websocket/" + "陈浪涛");
// websocket的发送和关闭函数
send.addEventListener('click', () => {
webSocket.send(JSON.stringify(msg.value));
})
close.addEventListener('click', () => {
webSocket.close();
})
// websocket的检测函数
webSocket.onopen = (event) => {
console.log("websocket连接成功")
}
webSocket.onmessage = (event) => {
// 数据类型自己搜
dialog.value = dialog.value + event.data;
}
webSocket.onerror = (event) => {
console.log("连接错误");
}
webSocket.onclose = (event) => {
console.log("当前连接已经关闭");
}
递归获取树型结构
- 在集合中过滤出所有的父亲id为此id的对象,并设为子元素
/**
* 返回树型结构数据
*/
public List<Object> getObjectTree() {
List<Object> list = new ArrayList();
list.stream.filter(obj -> obj.getParentId() = 0)
.map(obj -> {
obj.setChildren(setChildren(obj.getId(), list))
})
}
/**
* 递归设置子对象
* @param parent 父亲
* @param list 所有元素集合
* @return
*/
public List<Object> setChildren(Object parent, List<Object> list) {
return list.stream().filter(obj -> obj.getParentId() = parent.getId())
.map(obj -> {
obj.setChildren(obj.getId(), list);
return obj;
}).collect(Collectors.toList());
}
IDEA进行热加载的三种方式
方式
spring-boot-devtools(不推荐, 重启速度慢)IDEA自带HowSwap功能(推荐)JRebel插件(最推荐)
使用JRebel插件进行热加载
-
插件下载地址
https://plugins.jetbrains.com/plugin/4441-jrebel-and-xrebel/versions- 必须下载2022.4.1版本的
-
打开IDEA的插件设置,从磁盘下载好的zip包进行插件安装
-
重启IDEA
-
打开IDEA设置激活JRebel
- 地址填写
https://jrebel.qekang.com/1e67ec1b-122f-4708-87d0-c1995dc0cdaa - 邮箱随便写
- 地址填写
- 点击离线工作,避免因为网络问题导致激活失效
- 使用
- 使用JRebel的debug模式启动项目
- 在每次更新代码之后会自动更新,更新会有日志输出
设置统一API前缀
实现
- 使用webmvc中的
configurePathMatch方法进行设置 - 根据不同的包名给接口设置不同前缀admin包下的接口统一加上
/admin前缀,app包下的加上/app
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
AntPathMatcher matcher = new AntPathMatcher(".");
// 这里的c指定是每一个controller类
configurer.addPathPrefix("/admin", c -> {
return c.isAnnotationPresent(RestController.class) && matcher.match("**.controller.admin.**", c.getPackage().getName());
});
configurer.addPathPrefix("/app", c -> {
return c.isAnnotationPresent(RestController.class) && matcher.match("**.controller.app.**", c.getPackage().getName());
});
}
设置参数校验规则
背景
- 项目使用
Hibernate Validator框架,对restfulapi接口进行参数的校验,以保证最终数据入库的正确性
参数校验的注解
- Validator内置了20+的参数校验注解
| 注解 | 校验规则 |
|---|---|
@NotBlank |
检验字符串不为null,并且字符串trim以后的长度length需要大于0 |
@NotEmpty |
检验集合元素不为0,即集合不为空,也可以用于检验字符串不为null |
@NotNull |
检验对象不为null |
@Max(value) |
检验数字类型的最大值不能超过规定的value |
@Min(value) |
检验数字类型的最小值不能小于规定的value |
@Pattern(value) |
元素必须符合指定的正则表达式 |
@Range(min, max) |
检验数字在规定的范围内 |
@Size(max, min) |
检验字段的size是否在规定的范围内,可以是字符串,数组,集合,map等 |
@Length(max, min) |
检验字符串的长度是否在规定的范围内 |
@AssertFalse |
检验的字段必须为False |
@AssertTrue |
检验的字段必须为True |
@Email |
被注解的元素必须是电子邮件格式 |
@URL(protocol, host, port, regexp ,flags) |
被注解的元素必须是一个有效的URL |
| 注解 | 校验规则 |
|---|---|
| @Null | 必须为 null |
| @DecimalMax(value) | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
| @DecimalMin(value) | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 |
| @Digits(integer, fraction) | 被注释的元素必须是一个数字,其值必须在可接受的范围内 |
| @Positive | 判断正数 |
| @PositiveOrZero | 判断正数或 0 |
| @Negative | 判断负数 |
| @NegativeOrZero | 判断负数或 0 |
| @Future | 被注释的元素必须是一个将来的日期 |
| @FutureOrPresent | 判断日期是否是将来或现在日期 |
| @Past | 检查该字段的日期是在过去 |
| @PastOrPresent | 判断日期是否是过去或现在日期 |
| @SafeHtml | 判断日期是否是过去或现在日期 |
如何使用
- 导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
- 注意点:在
spring-boot-2.3.x之后的版本不再自动引入validateion的依赖,需要自己额外手动导入
-
在类上加上
@Validated注解- 对于web来说在Controller或则在ServiceImpl上面加上该注解,即可开启方法参数校验
@RestController @Validated public class TestController{ }// 对于service来说一般加在实现类上 @Validated public class LogServiceImpl implements LogService{ } -
对需要校验的参数进行规则设置
- 参数是普通类型的
// 对于Controller @RestController @RequestMapping("/log") public class LogController{ @GetMapping("/list") public Result list(@NotBlank(message="参数不为空") String message) { } } // 对于Service, 规则注解加在接口上 public interface LogService{ public List<LogVo> list(@NotBlank String message); }- 参数是对象类型
@RestControl @RequestMapping("/log") public class LogController{ @GetMapping("/list") public Result list(@Valid LogDto log) { } } // 对于对象类型,@Validated注解也可以加载LogDto对象 class LogDto{ @NotBlank(message = "操作名称不为空") private String optionName; @NotBlank(message = "ip地址不为空") private String ip; @NotEmpty(message = "在线人数不为空") private List<OnlinePerson> onLinePerson; }
捕获校验失败后的消息
- 统一异常处理
- 使用
getBindingResult().getAllErrors().stream().findFirst().get().getDefaultMessage()方法即可捕获message信息
@RestControllerAdvice
public class GlobalException{
public R handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
return R.faild(e.getBindingResult().getAllErrors().stream().findFirst().get().getDefaultMessage())
}
}
数据权限
如何做数据权限
数据权限原理
- 使用where进行条件限制达到数据隔离
常见的数权限实现方案
- 基于ServiceImpl进行硬编码拼接where条件
- 基于Mybatis进行动态sql条件的动态拼接(
两种方案)- 需要进行sql的重写
- 需要进行sql的解析
基于Mybatis数据权限方案实现
- 使用Mybatis的来节气Interceptor进行sql的改写执行,完成数据权限
- 实现Interceptor即可对要执行的sql进行拦截处理
- 使用相对复杂
public interface Interceptor {
Object intercept(Invocation var1) throws Throwable;
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
default void setProperties(Properties properties) {
}
}
基于Mybatis-Plus的拦截器进行实现
- Mybatis-Plus的拦截器InnerInceptor实现了原来Mybatis的Interceptor,进行了简化
- 使用相对于Mybatis方便
public interface InnerInterceptor {
// true:执行,false:不执行
default boolean willDoQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
return true;
}
// 在这里进行sql的改写
default void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
}
default boolean willDoUpdate(Executor executor, MappedStatement ms, Object parameter) throws SQLException {
return true;
}
default void beforeUpdate(Executor executor, MappedStatement ms, Object parameter) throws SQLException {
}
default void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {
}
default void setProperties(Properties properties) {
}
}
基于Mybatis-Plus的InnerInterceptor拦截器和JsqlParserSupport简单的数据权限条件的添加
sql拦截器
- 使用
InnerInterceptor进行sql的拦截 - 使用
JsqlParserSuppor进行sql的改造
@Slf4j
public class PermissionInterceptor extends JsqlParserSupport implements InnerInterceptor {
/**
* 拦截sql
*
* @param executor
* @param ms
* @param parameter
* @param rowBounds
* @param resultHandler
* @param boundSql
* @return
* @throws SQLException
*/
@Override
public boolean willDoQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
return InnerInterceptor.super.willDoQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql);
}
@Override
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
log.warn("拦截的sql为: \n{}", boundSql.getSql());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
log.warn("需要注入的参数: \n{}", parameterMappings);
PluginUtils.MPBoundSql mpBoundSql = PluginUtils.mpBoundSql(boundSql);
mpBoundSql.sql(parserSingle(mpBoundSql.sql(), null));
}
/**
* 使用JsqlParserSupport类的方法对拦截后的sql进行改造(CRUD)
*
* @param insert
* @param index
* @param sql
* @param obj
*/
@Override
protected void processInsert(Insert insert, int index, String sql, Object obj) {
super.processInsert(insert, index, sql, obj);
}
@Override
protected void processDelete(Delete delete, int index, String sql, Object obj) {
super.processDelete(delete, index, sql, obj);
}
@Override
protected void processUpdate(Update update, int index, String sql, Object obj) {
super.processUpdate(update, index, sql, obj);
}
@Override
protected void processSelect(Select select, int index, String sql, Object obj) {
log.warn("进入sql查询改造");
List<WithItem> withItemsList = select.getWithItemsList();
SelectBody selectBody = select.getSelectBody();
// 如果是普通查询
if (selectBody instanceof PlainSelect) {
PlainSelect plainSelect = (PlainSelect) selectBody;
List<SelectItem> selectItems = plainSelect.getSelectItems();
log.warn("详{}", selectItems);
AndExpression expression = null;
// 数据过滤条件
try {
expression = new AndExpression(plainSelect.getWhere(), CCJSqlParserUtil.parseCondExpression("log_system ='人员管理接口'"));
} catch (JSQLParserException e) {
e.printStackTrace();
}
plainSelect.setWhere(expression);
}
// 最终执行的sql:SELECT xxx WHERE is_deleted = 0 AND log_system = '人员管理接口' LIMIT ?
log.warn("查询体:{}", selectBody);
}
}
配置类(将自定义的数据权限拦截器注入mybatis拦截器机制中)
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PermissionInterceptor());
return interceptor;
}
}
如何设计好一套好的功能权限
目标
- 校验某个用户,能不能对系统进行某个操作
- 配置权限灵活
- 支持多种级别的权限
菜单权限按钮权限访问路径权限
常见的权限模型
-
RBAC-
基于角色的权限访问控制(Role-Based-Access-Control)
-
校验权限的流程:1.该操作有哪些角色拥有2.当前用户有哪些角色3.求交集,如果存在,则可以进行该操作
-
好处:简单,可复用
- RBAC权限模型常见的表设计
-
-
ABAC-
基于属性的访问控制(Attribute Based Access Control)
-
定义:一个操作是否被允许,是基于这四个信息共同动态计算决定的
对象:当前访问资源的对象资源:当前用户要访问的资源或者对象操作:当前对象对要访问的资源的读取,添加....环境信息:当前访问请求的上下文
-
示例:
- P5(职级)的研发(职位)同学,在公司内网的情况下可以查看和下载(操作)代码(资源)
- 这样的权限RBAC是做不到的
- P5(职级)的研发(职位)同学,在公司内网的情况下可以查看和下载(操作)代码(资源)
-
好处:
- 能够实现非常灵活的权限控制,几乎满足所有的要求
-
劣势
- 对于中小型企业来说,维护角色和权限关系的工作量不大,反而定制各种策略相对麻烦,更适合RBAC
-
常见的使用场景
-
比较适用于
用户数量多,授权比较复杂的场景- 列如:云系统:阿里云, AWS
-
-
-
ACL:linux的权限系统,较弱不推荐
JSqlParser解析器
- 作用:将指定的sql语句转换成java对象
基本用法
- 实现sql的
and,in,=等表达式操作
Select select = (Select) CCJSqlParserUtil.parse("select name, nick_name, password from user where is_deleted = 0");
PlainSelect plainSelect = (PlainSelect) select.getSelectBody();
Expression where = plainSelect.getWhere();
/**
* 构建in条件
*/
ExpressionList expressionList = new ExpressionList(Arrays.asList(new LongValue(1), new LongValue(2)));
InExpression deptIdsIn = new InExpression(new Column("dept_id"), expressionList);
log.warn("{}", deptIdsIn);
// dept_id IN (1, 2)
/**
* 构建=条件
*/
EqualsTo equalsTo = new EqualsTo();
equalsTo.setLeftExpression(new Column("name"));
equalsTo.setRightExpression(new StringValue("陈浪涛"));
log.warn("{}", equalsTo);
//name = '陈浪涛'
/**
* 括号操作
*/
Parenthesis parenthesis = new Parenthesis(equalsTo);
log.warn("{}", parenthesis);
// (name = '陈浪涛')
/**
* and操作
*/
AndExpression expression = new AndExpression(where, deptIdsIn);
log.warn("{}", expression);
// is_deleted = 0 AND dept_id IN (1, 2)
/**
* like操作
*/
LikeExpression likeExpression = new LikeExpression();
likeExpression.setLeftExpression(new Column("mail"));
likeExpression.setRightExpression(new StringValue("[3,1]_1%"));
log.warn("{}", likeExpression);
//mail LIKE '[3,1]_1%'
/**
* 字符串直接生成表达式
*/
Expression all = CCJSqlParserUtil.parseCondExpression("name = '陈浪涛' and dept_id in (1, 2)");
log.warn("{}", all);
//name = '陈浪涛' AND dept_id IN (1, 2)
核心用法
- 访问者模式
@Test
void visitor() throws JSQLParserException {
String sql = "select name, sex, dept_id from user where dept_id = 1";
CCJSqlParserManager manager = new CCJSqlParserManager();
Statement statement = manager.parse(new StringReader(sql));
statement.accept(new MyVisitor());
log.warn("改造后的sql{}", statement);
}
@Slf4j
class MyVisitor implements SelectVisitor, StatementVisitor {
@Override
public void visit(PlainSelect plainSelect) {
Expression where = plainSelect.getWhere();
log.warn("改造前的where条件;{}", where);
try {
Expression addCondition = CCJSqlParserUtil.parseCondExpression("dept_id in (1, 2, 3)");
plainSelect.setWhere(new AndExpression(where, addCondition));
log.warn("改造后的where条件:{}", addCondition);
} catch (JSQLParserException e) {
e.printStackTrace();
}
}
@Override
public void visit(Select select) {
SelectBody selectBody = select.getSelectBody();
if (selectBody != null) {
// 如果查询体不为空,直接执行这个类的处理普通查询的方法
selectBody.accept(this);
}
}
}
OAuth2.0授权
基本介绍
- OAuth2.0是一个授权协议,是系统和系统之间的用户的授权
授权模式的选择
大致了解
技术选型
- 可以使用开源的项目,也可以自己自研
开源项目
- Spring Security OAuth
- Spring Authorization Server
- 缺点
- 学习成本大
- 排查问题难
- 定制成本高
自研
- 解决Spring Authorization Server存在的问题
- 内置SSO功能(单点登录)
ruoyi-pro实现授权登录
基于授权码模式实现sso
SSO单点登录简介
- 指在多系统应用群中登录一个系统,便可在其他所有系统中得到授权而无需再次登录,包括单点登录与单点注销两部分
步骤
- 基于授权码模式实现sso单点登录
- 初始化
- 跳转sso登录
- 跳转回来,附带code授权码
- 获得访问令牌
思路
- 系统客户端带上参数请求后端获取授权码
function ssoLogin() {
const clientId = 'yudao-sso-demo-by-code'; // 可以改写成,你的 clientId
const redirectUri = encodeURIComponent('http://127.0.0.1:18080/callback.html'); // 注意,需要使用 encodeURIComponent 编码地址
const responseType = 'code'; // 1)授权码模式,对应 code;2)简化模式,对应 token
window.location.href = 'http://127.0.0.1:81/sso?client_id=' + clientId
+ '&redirect_uri=' + redirectUri
+ '&response_type=' + responseType;
}
- 后端通过
/system/oauth2/authorize接口处理返回授权码
public CommonResult<String> approveOrDeny(@RequestParam("response_type") String responseType,
@RequestParam("client_id") String clientId,
@RequestParam(value = "scope", required = false) String scope,
@RequestParam("redirect_uri") String redirectUri,
@RequestParam(value = "auto_approve") Boolean autoApprove,
@RequestParam(value = "state", required = false) String state) {
@SuppressWarnings("unchecked")
Map<String, Boolean> scopes = JsonUtils.parseObject(scope, Map.class);
scopes = ObjectUtil.defaultIfNull(scopes, Collections.emptyMap());
// 0. 校验用户已经登录。通过 Spring Security 实现
// 1.1 校验 responseType 是否满足 code 或者 token 值
OAuth2GrantTypeEnum grantTypeEnum = getGrantTypeEnum(responseType);
// 1.2 校验 redirectUri 重定向域名是否合法 + 校验 scope 是否在 Client 授权范围内
OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientId, null,
grantTypeEnum.getGrantType(), scopes.keySet(), redirectUri);
// 2.1 假设 approved 为 null,说明是场景一
if (Boolean.TRUE.equals(autoApprove)) {
// 如果无法自动授权通过,则返回空 url,前端不进行跳转
if (!oauth2ApproveService.checkForPreApproval(getLoginUserId(), getUserType(), clientId, scopes.keySet())) {
return success(null);
}
} else { // 2.2 假设 approved 非 null,说明是场景二
// 如果计算后不通过,则跳转一个错误链接
if (!oauth2ApproveService.updateAfterApproval(getLoginUserId(), getUserType(), clientId, scopes)) {
return success(OAuth2Utils.buildUnsuccessfulRedirect(redirectUri, responseType, state,
"access_denied", "User denied access"));
}
}
// 3.1 如果是 code 授权码模式,则发放 code 授权码,并重定向
List<String> approveScopes = convertList(scopes.entrySet(), Map.Entry::getKey, Map.Entry::getValue);
if (grantTypeEnum == OAuth2GrantTypeEnum.AUTHORIZATION_CODE) {
return success(getAuthorizationCodeRedirect(getLoginUserId(), client, approveScopes, redirectUri, state));
}
// 3.2 如果是 token 则是 implicit 简化模式,则发送 accessToken 访问令牌,并重定向
return success(getImplicitGrantRedirect(getLoginUserId(), client, approveScopes, redirectUri, state));
}
- 系统客户端携带授权码请求
/system/oauth2/token获取token
public CommonResult<OAuth2OpenAccessTokenRespVO> postAccessToken(HttpServletRequest request,
@RequestParam("grant_type") String grantType,
@RequestParam(value = "code", required = false) String code, // 授权码模式
@RequestParam(value = "redirect_uri", required = false) String redirectUri, // 授权码模式
@RequestParam(value = "state", required = false) String state, // 授权码模式
@RequestParam(value = "username", required = false) String username, // 密码模式
@RequestParam(value = "password", required = false) String password, // 密码模式
@RequestParam(value = "scope", required = false) String scope, // 密码模式
@RequestParam(value = "refresh_token", required = false) String refreshToken) { // 刷新模式
List<String> scopes = OAuth2Utils.buildScopes(scope);
// 1.1 校验授权类型
OAuth2GrantTypeEnum grantTypeEnum = OAuth2GrantTypeEnum.getByGranType(grantType);
if (grantTypeEnum == null) {
throw exception0(BAD_REQUEST.getCode(), StrUtil.format("未知授权类型({})", grantType));
}
if (grantTypeEnum == OAuth2GrantTypeEnum.IMPLICIT) {
throw exception0(BAD_REQUEST.getCode(), "Token 接口不支持 implicit 授权模式");
}
// 1.2 校验客户端
String[] clientIdAndSecret = obtainBasicAuthorization(request);
OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientIdAndSecret[0], clientIdAndSecret[1],
grantType, scopes, redirectUri);
// 2. 根据授权模式,获取访问令牌
OAuth2AccessTokenDO accessTokenDO;
switch (grantTypeEnum) {
case AUTHORIZATION_CODE:
accessTokenDO = oauth2GrantService.grantAuthorizationCodeForAccessToken(client.getClientId(), code, redirectUri, state);
break;
case PASSWORD:
accessTokenDO = oauth2GrantService.grantPassword(username, password, client.getClientId(), scopes);
break;
case CLIENT_CREDENTIALS:
accessTokenDO = oauth2GrantService.grantClientCredentials(client.getClientId(), scopes);
break;
case REFRESH_TOKEN:
accessTokenDO = oauth2GrantService.grantRefreshToken(refreshToken, client.getClientId());
break;
default:
throw new IllegalArgumentException("未知授权类型:" + grantType);
}
Assert.notNull(accessTokenDO, "访问令牌不能为空"); // 防御性检查
return success(OAuth2OpenConvert.INSTANCE.convert(accessTokenDO));
}
- 客户端获取token以后即可访问系统资源
结合阿里短信发送
阿里云的用户权限操作
- 用户通过个人账号或者企业账号对阿里的服务实现统一调用
- 开启子用户
- 新建一个用户组并添加相关的服务调用权限
- 创建一个用户
- 获取AccessKey和密码用于代码编程
短信验证服务
- 购买短信验证服务
- 搜索短信验证服务控制台进行短信的编辑
- 为短信添加签名和模板即可
- 等待审核
编码实现短信发送
- 前提条件获取阿里的
Accesskey和密码
private static String ACCESS_KEY = "xxx";
private static String KEY_SECRET = "xxx";
private static String PHONE_NUMBER = "xxx";
private static String TEMPLATE_NAME = "xxx";
private static String SIGN_NAME = "xxx";
/**
* 使用AK&SK初始化账号Client
* @param accessKeyId
* @param accessKeySecret
* @return Client
* @throws Exception
*/
public static com.aliyun.dysmsapi20170525.Client createClient(String accessKeyId, String accessKeySecret) throws Exception {
com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config()
// 必填,您的 AccessKey ID
.setAccessKeyId(accessKeyId)
// 必填,您的 AccessKey Secret
.setAccessKeySecret(accessKeySecret);
// Endpoint 请参考 https://api.aliyun.com/product/Dysmsapi
config.endpoint = "dysmsapi.aliyuncs.com";
return new com.aliyun.dysmsapi20170525.Client(config);
}
public static void main(String[] args_) throws Exception {
java.util.List<String> args = java.util.Arrays.asList(args_);
// 请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_ID 和 ALIBABA_CLOUD_ACCESS_KEY_SECRET。
// 工程代码泄露可能会导致 AccessKey 泄露,并威胁账号下所有资源的安全性。以下代码示例使用环境变量获取 AccessKey 的方式进行调用,仅供参考,建议使用更安全的 STS 方式,更多鉴权访问方式请参见:https://help.aliyun.com/document_detail/378657.html
com.aliyun.dysmsapi20170525.Client client = SMS.createClient(ACCESS_KEY, KEY_SECRET);
com.aliyun.dysmsapi20170525.models.SendSmsRequest sendSmsRequest = new com.aliyun.dysmsapi20170525.models.SendSmsRequest();
// 短信设置(手机号,签名,短信模板,验证码)
sendSmsRequest.setPhoneNumbers(PHONE_NUMBER);
sendSmsRequest.setTemplateCode(TEMPLATE_NAME);
sendSmsRequest.setSignName(SIGN_NAME);
JSONObject obj = JSONUtil.createObj();
obj.put("code", RandomUtil.randomNumbers(4));
sendSmsRequest.setTemplateParam(obj.toString());
// sendSmsRequest.setTemplateParam("{\"code\":\"1234\"}") ;
com.aliyun.teautil.models.RuntimeOptions runtime = new com.aliyun.teautil.models.RuntimeOptions();
try {
// 复制代码运行请自行打印 API 的返回值
SendSmsResponse sendSmsResponse = client.sendSmsWithOptions(sendSmsRequest, runtime);
System.out.println(sendSmsResponse.getStatusCode());
System.out.println(sendSmsResponse.getBody());
} catch (TeaException error) {
// 错误 message
System.out.println(error.getMessage());
// 诊断地址
System.out.println(error.getData().get("Recommend"));
com.aliyun.teautil.Common.assertAsString(error.message);
} catch (Exception _error) {
TeaException error = new TeaException(_error.getMessage(), _error);
// 错误 message
System.out.println(error.getMessage());
// 诊断地址
System.out.println(error.getData().get("Recommend"));
com.aliyun.teautil.Common.assertAsString(error.message);
}
}
浙公网安备 33010602011771号