规范:前后端接口规范
1、前言
随着互联网的高速发展,前端页面的展示、交互体验越来越灵活、炫丽,响应体验也要求越来越高,后端服务的高并发、高可用、高性能、高扩展等特性的要求也愈加苛刻,从而导致前后端研发各自专注于自己擅长的领域深耕细作。
然而带来的另一个问题:前后端的对接界面双方却关注甚少,没有任何接口约定规范情况下各自干各自的,导致我们在产品项目开发过程中,前后端的接口联调对接工作量占比在30%-50%左右,甚至会更高。往往前后端接口联调对接及系统间的联调对接都是整个产品项目研发的软肋。
本文的主要初衷就是规范约定先行,尽量避免沟通联调产生的不必要的问题,让大家身心愉快地专注于各自擅长的领域。
2、为何要分离
目前现有前后端开发模式:“后端为主的MVC时代”,如下图所示:
后端为主的MVC时代
代码可维护性得到明显好转,MVC 是个非常好的协作模式,从架构层面让开发者懂得什么代码应该写在什么地方。为了让 View 层更简单干脆,还可以选择 Velocity、Freemaker 等模板,使得模板里写不了 Java 代码。
看起来是功能变弱了,但正是这种限制使得前后端分工更清晰。然而依旧并不是那么清晰,这个阶段的典型问题是:
前端开发重度依赖开发环境,开发效率低。
这种架构下,前后端协作有两种模式:一种是前端写demo,写好后,让后端去套模板 。淘宝早期包括现在依旧有大量业务线是这种模式。好处很明显,demo 可以本地开发,很高效。不足是还需要后端套模板,有可能套错,套完后还需要前端确定,来回沟通调整的成本比较大。
另一种协作模式是前端负责浏览器端的所有开发和服务器端的 View 层模板开发,支付宝是这种模式。好处是 UI 相关的代码都是前端去写就好,后端不用太关注,不足就是前端开发重度绑定后端环境,环境成为影响前端开发效率的重要因素。
前后端职责依旧纠缠不清。
Velocity 模板还是蛮强大的,变量、逻辑、宏等特性,依旧可以通过拿到的上下文变量来实现各种业务逻辑。这样,只要前端弱势一点,往往就会被后端要求在模板层写出不少业务代码。
还有一个很大的灰色地带是 Controller,页面路由等功能本应该是前端最关注的,但却是由后端来实现。Controller 本身与 Model 往往也会纠缠不清,看了让人咬牙的业务代码经常会出现在 Controller 层。这些问题不能全归结于程序员的素养,否则 JSP 就够了。
对前端发挥的局限。
性能优化如果只在前端做空间非常有限,于是我们经常需要后端合作才能碰撞出火花,但由于后端框架限制,我们很难使用Comet、Bigpipe等技术方案来优化性能。
总上所述,就跟为什麽要代码重构一样:
-
关注点分离
-
职责分离
-
对的人做对的事
-
更好的共建模式
-
快速的反应变化
3、什么是分离
我们现在要做的前后分离第一阶段:“基于 Ajax 带来的 SPA 时代”,如图:
基于 Ajax 带来的 SPA 时代
这种模式下,前后端的分工非常清晰,前后端的关键协作点是 Ajax 接口。看起来是如此美妙,但回过头来看看的话,这与 JSP 时代区别不大。复杂度从服务端的 JSP 里移到了浏览器的 JavaScript,浏览器端变得很复杂。类似 Spring MVC,这个时代开始出现浏览器端的分层架构:
浏览器端的分层架构
对于这一SPA阶段,前后端分离有几个重要挑战:
前后端接口的约定。
如果后端的接口一塌糊涂,如果后端的业务模型不够稳定,那么前端开发会很痛苦。这一块在业界有 API Blueprint 等方案来约定和沉淀接口,==在阿里,不少团队也有类似尝试,通过接口规则、接口平台等方式来做。有了和后端一起沉淀的接口规则,还可以用来模拟数据,使得前后端可以在约定接口后实现高效并行开发。== 相信这一块会越做越好。
前端开发的复杂度控制。
SPA 应用大多以功能交互型为主,JavaScript 代码过十万行很正常。大量 JS 代码的组织,与 View 层的绑定等,都不是容易的事情。典型的解决方案是业界的 Backbone,但 Backbone 做的事还很有限,依旧存在大量空白区域需要挑战。
4、如何做分离
4.1 职责分离
职责分离
-
前后端仅仅通过异步接口(AJAX/JSONP)来编程
-
前后端都各自有自己的开发流程,构建工具,测试集合
-
关注点分离,前后端变得相对独立并松耦合
4.2 开发流程
-
后端编写和维护接口文档,在 API 变化时更新接口文档
-
后端根据接口文档进行接口开发
-
前端根据接口文档进行开发 + Mock平台
-
开发完成后联调和提交测试
Mock 服务器根据接口文档自动生成 Mock 数据,实现了接口文档即API:
开发流程
4.3 具体实施
现在已基本完成了,接口方面的实施:
-
接口文档服务器:可实现接口变更实时同步给前端展示;
-
Mock接口数据平台:可实现接口变更实时Mock数据给前端使用;
-
接口规范定义:很重要,接口定义的好坏直接影响到前端的工作量和实现逻辑;具体定义规范见下节;
接口文档+Mock平台服务器
5. 接口规范V1.0.0
5.1 规范原则
-
接口返回数据即显示:前端仅做渲染逻辑处理;
-
渲染逻辑禁止跨多个接口调用;
-
前端关注交互、渲染逻辑,尽量避免业务逻辑处理的出现;
-
请求响应传输数据格式:JSON,JSON数据尽量简单轻量,避免多级JSON的出现;
5.2 基本格式
5.2.1 请求基本格式
GET请求、POST请求==必须包含key为body的入参,所有请求数据包装为JSON格式,并存放到入参body中==,示例如下:
GET请求:
xxx/login?body={"username":"admin","password":"123456","captcha":"scfd","rememberMe":1}
POST请求:
5.2.2 响应基本格式
{code: 200,data: {message: "success"}
}
code : 请求处理状态
-
200: 请求处理成功
-
500: 请求处理失败
-
401: 请求未认证,跳转登录页
-
406: 请求未授权,跳转未授权提示页
data.message: 请求处理消息
-
code=200 且 data.message="success": 请求处理成功
-
code=200 且 data.message!="success": 请求处理成功, 普通消息提示:message内容。
-
code=500: 请求处理失败,警告消息提示:message内容
5.3 响应实体格式
{code: 200,data: {message: "success",entity: {id: 1,name: "XXX",code: "XXX"}}
}
data.entity: 响应返回的实体数据
5.4 响应列表格式
data.list: 响应返回的列表数据
5.5 响应分页格式
{code: 200,data: {recordCount: 2,message: "success",totalCount: 2,pageNo: 1,pageSize: 10,list: \[{id: 1,name: "XXX",code: "H001"},{id: 2,name: "XXX",code: "H001"} \],totalPage: 1}
}
-
data.recordCount: 当前页记录数
-
data.totalCount: 总记录数
-
data.pageNo: 当前页码
-
data.pageSize: 每页大小
-
data.totalPage: 总页数
5.6 特殊内容规范
5.6.1 下拉框、复选框、单选框
由后端接口统一逻辑判定是否选中,通过isSelect标示是否选中,示例如下:
{code: 200,data: {message: "success",list: \[{id: 1,name: "XXX",code: "XXX",isSelect: 1}, {id: 1,name: "XXX",code: "XXX",isSelect: 0}\]}
}
禁止下拉框、复选框、单选框判定选中逻辑由前端来处理,统一由后端逻辑判定选中返回给前端展示;
5.6.2 Boolean类型
关于Boolean类型,JSON数据传输中一律使用1/0来标示,1为是/True,0为否/False;
5.6.3 日期类型
关于日期类型,JSON数据传输中一律使用字符串,具体日期格式因业务而定;
6、未来的大前端
目前我们现在用的前后端分离模式属于第一阶段,由于使用到的一些技术jquery等,对于一些页面展示、数据渲染还是比较复杂,不能够很好的达到复用。对于前端还是有很大的工作量。
下一阶段可以在前端工程化方面,对技术框架的选择、前端模块化重用方面,可多做考量。也就是要迎来“==前端为主的 MV* 时代==”。大多数的公司也基本都处于这个分离阶段。
最后阶段就是==Node 带来的全栈时代==,完全有前端来控制页面,URL,Controller,路由等,后端的应用就逐步弱化为真正的数据服务+业务服务,做且仅能做的是提供数据、处理业务逻辑,关注高可用、高并发等。
Spring Boot 后端接口规范
一个后端接口大致分为四个部分组成:接口地址(url)、接口请求方式(get、post等)、请求数据(request)、响应数据(response)。虽然说后端接口的编写并没有统一规范要求,而且如何构建这几个部分每个公司要求都不同,没有什么“一定是最好的”标准,但其中最重要的关键点就是看是否规范。
环境说明
因为讲解的重点是后端接口,所以需要导入一个spring-boot-starter-web
包,而lombok作用是简化类,前端显示则使用了knife4j,具体使用在Spring Boot整合knife4j实现Api文档已写明。另外从springboot-2.3开始,校验包被独立成了一个starter组件,所以需要引入如下依赖:
<dependency>
<!--新版框架没有自动引入需要手动引入--><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId>
</dependency><dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-spring-boot-starter</artifactId><!--在引用时请在maven中央仓库搜索最新版本号--><version>2.0.2</version>
</dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional>
</dependency>
参数校验
1、介绍
一个接口一般对参数(请求数据)都会进行安全校验,参数校验的重要性自然不必多说,那么如何对参数进行校验就有讲究了。一般来说有三种常见的校验方式,我们使用了最简洁的第三种方法
-
业务层校验
-
Validator + BindResult校验
-
Validator + 自动抛出异常
业务层校验无需多说,即手动在java的Service层进行数据校验判断。不过这样太繁琐了,光校验代码就会有很多
而使用Validator+ BindingResult
已经是非常方便实用的参数校验方式了,在实际开发中也有很多项目就是这么做的,不过这样还是不太方便,因为你每写一个接口都要添加一个BindingResult参数,然后再提取错误信息返回给前端(简单看一下)。
/**
* @author 公众号:码猿技术专栏
*/
@PostMapping("/addUser")
public String addUser(@RequestBody @Validated User user, BindingResult bindingResult) {// 如果有参数校验失败,会将错误信息封装成对象组装在BindingResult里List<ObjectError> allErrors = bindingResult.getAllErrors();if(!allErrors.isEmpty()){return allErrors.stream().map(o->o.getDefaultMessage()).collect(Collectors.toList()).toString();}// 返回默认的错误信息// return allErrors.get(0).getDefaultMessage();return validationService.addUser(user);
}
2、Validator + 自动抛出异常(使用)
内置参数校验如下:
首先Validator可以非常方便的制定校验规则,并自动帮你完成校验。首先在入参里需要校验的字段加上注解,每个注解对应不同的校验规则,并可制定校验失败后的信息:
@Data
public class User {@NotNull(message = "用户id不能为空")private Long id;@NotNull(message = "用户账号不能为空")@Size(min = 6, max = 11, message = "账号长度必须是6-11个字符")private String account;@NotNull(message = "用户密码不能为空")@Size(min = 6, max = 11, message = "密码长度必须是6-16个字符")private String password;@NotNull(message = "用户邮箱不能为空")@Email(message = "邮箱格式不正确")private String email;
}
校验规则和错误提示信息配置完毕后,接下来只需要在接口仅需要在校验的参数上加上@Valid
注解(去掉BindingResult后会自动引发异常,异常发生了自然而然就不会执行业务逻辑):
@RestController
@RequestMapping("user")
public class ValidationController {@Autowiredprivate ValidationService validationService;@PostMapping("/addUser")public String addUser(@RequestBody @Validated User user) {return validationService.addUser(user);}
}
现在我们进行测试,打开knife4j文档地址,当输入的请求数据为空时,Validator会将所有的报错信息全部进行返回,所以需要与全局异常处理一起使用。
// 使用form data方式调用接口,校验异常抛出 BindException
// 使用 json 请求体调用接口,校验异常抛出 MethodArgumentNotValidException
// 单个参数校验异常抛出ConstraintViolationException
// 处理 json 请求体调用接口校验失败抛出的异常
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResultVO<String> MethodArgumentNotValidException(MethodArgumentNotValidException e) {List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();List<String> collect = fieldErrors.stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.toList());return new ResultVO(ResultCode.VALIDATE_FAILED, collect);
}
// 使用form data方式调用接口,校验异常抛出 BindException
@ExceptionHandler(BindException.class)
public ResultVO<String> BindException(BindException e) {List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();List<String> collect = fieldErrors.stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.toList());return new ResultVO(ResultCode.VALIDATE_FAILED, collect);
}
3、分组校验和递归校验
分组校验有三个步骤:
-
定义一个分组类(或接口)
-
在校验注解上添加groups属性指定分组
-
Controller方法的@Validated注解添加分组类
public interface Update extends Default{
}
@Data
public class User {@NotNull(message = "用户id不能为空",groups = Update.class)private Long id;......
}
@PostMapping("update")
public String update(@Validated({Update.class}) User user) {return "success";
}
如果Update不继承Default,@Validated({Update.class})
就只会校验属于Update.class
分组的参数字段;如果继承了,会校验了其他默认属于Default.class
分组的字段。关注工众号:码猿技术专栏,回复关键词:1111 获取阿里内部java性能调优手册!
对于递归校验(比如类中类),只要在相应属性类上增加@Valid
注解即可实现(对于集合同样适用)
4、自定义校验
Spring Validation允许用户自定义校验,实现很简单,分两步:
-
自定义校验注解
-
编写校验者类
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {HaveNoBlankValidator.class})// 标明由哪个类执行校验逻辑
public @interface HaveNoBlank {// 校验出错时默认返回的消息String message() default "字符串中不能含有空格";Class<?>[] groups() default { };Class<? extends Payload>[] payload() default { };/*** 同一个元素上指定多个该注解时使用*/@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })@Retention(RUNTIME)@Documentedpublic @interface List {NotBlank[] value();}
}
public class HaveNoBlankValidator implements ConstraintValidator<HaveNoBlank, String> {@Overridepublic boolean isValid(String value, ConstraintValidatorContext context) {// null 不做检验if (value == null) {return true;}// 校验失败return !value.contains(" ");// 校验成功}
}
全局异常处理
参数校验失败会自动引发异常,我们当然不可能再去手动捕捉异常进行处理。但我们又不想手动捕捉这个异常,又要对这个异常进行处理,那正好使用SpringBoot全局异常处理来达到一劳永逸的效果!
1、基本使用
首先,我们需要新建一个类,在这个类上加上@ControllerAdvice
或@RestControllerAdvice
注解,这个类就配置成全局处理类了。
这个根据你的Controller层用的是
@Controller
还是@RestController
来决定。
然后在类中新建方法,在方法上加上@ExceptionHandler
注解并指定你想处理的异常类型,接着在方法内编写对该异常的操作逻辑,就完成了对该异常的全局处理!我们现在就来演示一下对参数校验失败抛出的MethodArgumentNotValidException
全局处理:
/**
* @author 公众号:码猿技术专栏
*/
@RestControllerAdvice
@ResponseBody
public class ExceptionControllerAdvice {@ExceptionHandler(MethodArgumentNotValidException.class)@ResponseStatus(HttpStatus.BAD_REQUEST)public String MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {// 从异常对象中拿到ObjectError对象ObjectError objectError = e.getBindingResult().getAllErrors().get(0);// 然后提取错误提示信息进行返回return objectError.getDefaultMessage();}/*** 系统异常 预期以外异常*/@ExceptionHandler(Exception.class)@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)public ResultVO<?> handleUnexpectedServer(Exception ex) {log.error("系统异常:", ex);// GlobalMsgEnum.ERROR是我自己定义的枚举类return new ResultVO<>(GlobalMsgEnum.ERROR);}/*** 所以异常的拦截*/@ExceptionHandler(Throwable.class)@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)public ResultVO<?> exception(Throwable ex) {log.error("系统异常:", ex);return new ResultVO<>(GlobalMsgEnum.ERROR);}
}
我们再次进行测试,这次返回的就是我们制定的错误提示信息!我们通过全局异常处理优雅的实现了我们想要的功能!
以后我们再想写接口参数校验,就只需要在入参的成员变量上加上Validator校验规则注解,然后在参数上加上@Valid
注解即可完成校验,校验失败会自动返回错误提示信息,无需任何其他代码!
2、自定义异常
在很多情况下,我们需要手动抛出异常,比如在业务层当有些条件并不符合业务逻辑,而使用自定义异常有诸多优点:
-
自定义异常可以携带更多的信息,不像这样只能携带一个字符串。
-
项目开发中经常是很多人负责不同的模块,使用自定义异常可以统一了对外异常展示的方式。关注工众号:码猿技术专栏,回复关键词:1111 获取阿里内部java性能调优手册!
-
自定义异常语义更加清晰明了,一看就知道是项目中手动抛出的异常。
我们现在就来开始写一个自定义异常:
/**
* @author 公众号:码猿技术专栏
*/
@Getter //只要getter方法,无需setter
public class APIException extends RuntimeException {private int code;private String msg;public APIException() {this(1001, "接口错误");}public APIException(String msg) {this(1001, msg);}public APIException(int code, String msg) {super(msg);this.code = code;this.msg = msg;}
}
然后在刚才的全局异常类中加入如下:
//自定义的全局异常@ExceptionHandler(APIException.class)public String APIExceptionHandler(APIException e) {return e.getMsg();}
这样就对异常的处理就比较规范了,当然还可以添加对Exception的处理,这样无论发生什么异常我们都能屏蔽掉然后响应数据给前端,不过建议最后项目上线时这样做,能够屏蔽掉错误信息暴露给前端,在开发中为了方便调试还是不要这样做。
另外,当我们抛出自定义异常的时候全局异常处理只响应了异常中的错误信息msg给前端,并没有将错误代码code返回。这还需要配合数据统一响应。
如果在多模块使用,全局异常等公共功能抽象成子模块,则在需要的子模块中需要将该模块包扫描加入,@SpringBootApplication(scanBasePackages = {"com.xxx"})
数据统一响应
统一数据响应是我们自己自定义一个响应体类,无论后台是运行正常还是发生异常,响应给前端的数据格式是不变的!这里我包括了响应信息代码code和响应信息说明msg,首先可以设置一个枚举规范响应体中的响应码和响应信息。
/**
* @author 公众号:码猿技术专栏
*/
@Getter
public enum ResultCode {SUCCESS(1000, "操作成功"),FAILED(1001, "响应失败"),VALIDATE_FAILED(1002, "参数校验失败"),ERROR(5000, "未知错误");private int code;private String msg;ResultCode(int code, String msg) {this.code = code;this.msg = msg;}
}
自定义响应体
package com.csdn.demo1.global;import lombok.Getter;
/**
* @author 公众号:码猿技术专栏
*/
@Getter
public class ResultVO<T> {/*** 状态码,比如1000代表响应成功*/private int code;/*** 响应信息,用来说明响应情况*/private String msg;/*** 响应的具体数据*/private T data;public ResultVO(T data) {this(ResultCode.SUCCESS, data);}public ResultVO(ResultCode resultCode, T data) {this.code = resultCode.getCode();this.msg = resultCode.getMsg();this.data = data;}
}
最后需要修改全局异常处理类的返回类型
@RestControllerAdvice
public class ExceptionControllerAdvice {@ExceptionHandler(APIException.class)public ResultVO<String> APIExceptionHandler(APIException e) {// 注意哦,这里传递的响应码枚举return new ResultVO<>(ResultCode.FAILED, e.getMsg());}@ExceptionHandler(MethodArgumentNotValidException.class)public ResultVO<String> MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {ObjectError objectError = e.getBindingResult().getAllErrors().get(0);// 注意哦,这里传递的响应码枚举return new ResultVO<>(ResultCode.VALIDATE_FAILED, objectError.getDefaultMessage());}
}
最后在controller层进行接口信息数据的返回
@GetMapping("/getUser")
public ResultVO<User> getUser() {User user = new User();user.setId(1L);user.setAccount("12345678");user.setPassword("12345678");user.setEmail("123@qq.com");return new ResultVO<>(user);
}
经过测试,这样响应码和响应信息只能是枚举规定的那几个,就真正做到了响应数据格式、响应码和响应信息规范化、统一化!
还有一种全局返回类如下
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Msg {//状态码private int code;//提示信息private String msg;//用户返回给浏览器的数据private Map<String,Object> data = new HashMap<>();public static Msg success() {Msg result = new Msg();result.setCode(200);result.setMsg("请求成功!");return result;}public static Msg fail() {Msg result = new Msg();result.setCode(400);result.setMsg("请求失败!");return result;}public static Msg fail(String msg) {Msg result = new Msg();result.setCode(400);result.setMsg(msg);return result;}public Msg(ReturnResult returnResult){code = returnResult.getCode();msg = returnResult.getMsg();}public Msg add(String key,Object value) {this.getData().put(key, value);return this;}
}
全局处理响应数据(可选择)
接口返回统一响应体 + 异常也返回统一响应体,其实这样已经很好了,但还是有可以优化的地方。要知道一个项目下来定义的接口搞个几百个太正常不过了,要是每一个接口返回数据时都要用响应体来包装一下好像有点麻烦,有没有办法省去这个包装过程呢?
当然是有的,还是要用到全局处理。但是为了扩展性,就是允许绕过数据统一响应(这样就可以提供多方使用),我们可以自定义注解,利用注解来选择是否进行全局响应包装
首先创建自定义注解,作用相当于全局处理类开关:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD}) // 表明该注解只能放在方法上
public @interface NotResponseBody {
}
其次创建一个类并加上注解使其成为全局处理类。然后继承ResponseBodyAdvice
接口重写其中的方法,即可对我们的controller进行增强操作,具体看代码和注释:
package com.csdn.demo1.global;import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
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.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;@RestControllerAdvice(basePackages = {"com.scdn.demo1.controller"}) // 注意哦,这里要加上需要扫描的包
public class ResponseControllerAdvice implements ResponseBodyAdvice<Object> {@Overridepublic boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> aClass) {// 如果接口返回的类型本身就是ResultVO那就没有必要进行额外的操作,返回false// 如果方法上加了我们的自定义注解也没有必要进行额外的操作return !(returnType.getParameterType().equals(ResultVO.class) || returnType.hasMethodAnnotation(NotResponseBody.class));}@Overridepublic Object beforeBodyWrite(Object data, MethodParameter returnType, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest request, ServerHttpResponse response) {// String类型不能直接包装,所以要进行些特别的处理if (returnType.getGenericParameterType().equals(String.class)) {ObjectMapper objectMapper = new ObjectMapper();try {// 将数据包装在ResultVO里后,再转换为json字符串响应给前端return objectMapper.writeValueAsString(new ResultVO<>(data));} catch (JsonProcessingException e) {throw new APIException("返回String类型错误");}}// 将原本的数据包装在ResultVO里return new ResultVO<>(data);}
}
重写的这两个方法是用来在controller将数据进行返回前进行增强操作,supports方法要返回为true才会执行beforeBodyWrite
方法,所以如果有些情况不需要进行增强操作可以在supports方法里进行判断。
对返回数据进行真正的操作还是在beforeBodyWrite
方法中,我们可以直接在该方法里包装数据,这样就不需要每个接口都进行数据包装了,省去了很多麻烦。此时controller只需这样写就行了:
@GetMapping("/getUser")
//@NotResponseBody //是否绕过数据统一响应开关
public User getUser() {User user = new User();user.setId(1L);user.setAccount("12345678");user.setPassword("12345678");user.setEmail("123@qq.com");// 注意哦,这里是直接返回的User类型,并没有用ResultVO进行包装return user;
}
接口版本控制
1、简介
在spring boot项目中,如果要进行restful接口的版本控制一般有以下几个方向:
-
基于path的版本控制
-
基于header的版本控制
在spring MVC下,url映射到哪个method是由RequestMappingHandlerMapping
来控制的,那么我们也是通过 RequestMappingHandlerMapping
来做版本控制的。
2、Path控制实现
首先定义一个注解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiVersion {// 默认接口版本号1.0开始,这里我只做了两级,多级可在正则进行控制String value() default "1.0";
}
ApiVersionCondition
用来控制当前request 指向哪个method
public class ApiVersionCondition implements RequestCondition<ApiVersionCondition> {private static final Pattern VERSION_PREFIX_PATTERN = Pattern.compile("v(\\d+\\.\\d+)");private final String version;public ApiVersionCondition(String version) {this.version = version;}@Overridepublic ApiVersionCondition combine(ApiVersionCondition other) {// 采用最后定义优先原则,则方法上的定义覆盖类上面的定义return new ApiVersionCondition(other.getApiVersion());}@Overridepublic ApiVersionCondition getMatchingCondition(HttpServletRequest httpServletRequest) {Matcher m = VERSION_PREFIX_PATTERN.matcher(httpServletRequest.getRequestURI());if (m.find()) {String pathVersion = m.group(1);// 这个方法是精确匹配if (Objects.equals(pathVersion, version)) {return this;}// 该方法是只要大于等于最低接口version即匹配成功,需要和compareTo()配合// 举例:定义有1.0/1.1接口,访问1.2,则实际访问的是1.1,如果从小开始那么排序反转即可
// if(Float.parseFloat(pathVersion)>=Float.parseFloat(version)){
// return this;
// }}return null;}@Overridepublic int compareTo(ApiVersionCondition other, HttpServletRequest request) {return 0;// 优先匹配最新的版本号,和getMatchingCondition注释掉的代码同步使用
// return other.getApiVersion().compareTo(this.version);}public String getApiVersion() {return version;}}
PathVersionHandlerMapping
用于注入spring用来管理
public class PathVersionHandlerMapping extends RequestMappingHandlerMapping {@Overrideprotected boolean isHandler(Class<?> beanType) {return AnnotatedElementUtils.hasAnnotation(beanType, Controller.class);}@Overrideprotected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {ApiVersion apiVersion = AnnotationUtils.findAnnotation(handlerType,ApiVersion.class);return createCondition(apiVersion);}@Overrideprotected RequestCondition<?> getCustomMethodCondition(Method method) {ApiVersion apiVersion = AnnotationUtils.findAnnotation(method,ApiVersion.class);return createCondition(apiVersion);}private RequestCondition<ApiVersionCondition>createCondition(ApiVersion apiVersion) {return apiVersion == null ? null : new ApiVersionCondition(apiVersion.value());}
}
WebMvcConfiguration
配置类让spring来接管
@Configuration
public class WebMvcConfiguration implements WebMvcRegistrations {@Overridepublic RequestMappingHandlerMapping getRequestMappingHandlerMapping() {return new PathVersionHandlerMapping();}
}
最后controller进行测试,默认是v1.0,如果方法上有注解,以方法上的为准(该方法vx.x在路径任意位置出现都可解析)
@RestController
@ApiVersion
@RequestMapping(value = "/{version}/test")
public class TestController {@GetMapping(value = "one")public String query(){return "test api default";}@GetMapping(value = "one")@ApiVersion("1.1")public String query2(){return "test api v1.1";}@GetMapping(value = "one")@ApiVersion("3.1")public String query3(){return "test api v3.1";}
}
3、header控制实现
总体原理与Path类似,修改ApiVersionCondition
即可,之后访问时在header带上X-VERSION
参数即可
public class ApiVersionCondition implements RequestCondition<ApiVersionCondition> {private static final String X_VERSION = "X-VERSION";private final String version ;public ApiVersionCondition(String version) {this.version = version;}@Overridepublic ApiVersionCondition combine(ApiVersionCondition other) {// 采用最后定义优先原则,则方法上的定义覆盖类上面的定义return new ApiVersionCondition(other.getApiVersion());}@Overridepublic ApiVersionCondition getMatchingCondition(HttpServletRequest httpServletRequest) {String headerVersion = httpServletRequest.getHeader(X_VERSION);if(Objects.equals(version,headerVersion)){return this;}return null;}@Overridepublic int compareTo(ApiVersionCondition apiVersionCondition, HttpServletRequest httpServletRequest) {return 0;}public String getApiVersion() {return version;}}
API接口安全
1、简介
APP、前后端分离项目都采用API接口形式与服务器进行数据通信,传输的数据被偷窥、被抓包、被伪造时有发生,关注工众号:码猿技术专栏,回复关键词:1111 获取阿里内部java性能调优手册!;那么如何设计一套比较安全的API接口方案至关重要,一般的解决方案有以下几点:
-
Token授权认证,防止未授权用户获取数据;
-
时间戳超时机制;
-
URL签名,防止请求参数被篡改;
-
防重放,防止接口被第二次请求,防采集;
-
采用HTTPS通信协议,防止数据明文传输;
2、Token授权认证
因为HTTP协议是无状态的,Token的设计方案是用户在客户端使用用户名和密码登录后,服务器会给客户端返回一个Token,并将Token以键值对的形式存放在缓存(一般是Redis)中,后续客户端对需要授权模块的所有操作都要带上这个Token,服务器端接收到请求后进行Token验证,如果Token存在,说明是授权的请求。
Token生成的设计要求
-
应用内一定要唯一,否则会出现授权混乱,A用户看到了B用户的数据;
-
每次生成的Token一定要不一样,防止被记录,授权永久有效;
-
一般Token对应的是Redis的key,value存放的是这个用户相关缓存信息,比如:用户的id;
-
要设置Token的过期时间,过期后需要客户端重新登录,获取新的Token,如果Token有效期设置较短,会反复需要用户登录,体验比较差,我们一般采用Token过期后,客户端静默登录的方式,当客户端收到Token过期后,客户端用本地保存的用户名和密码在后台静默登录来获取新的Token,还有一种是单独出一个刷新Token的接口,但是一定要注意刷新机制和安全问题;
根据上面的设计方案要求,我们很容易得到Token=md5(用户ID+登录的时间戳+服务器端秘钥)这种方式来获得Token,因为用户ID是应用内唯一的,登录的时间戳保证每次登录的时候都不一样,服务器端秘钥是配置在服务器端参与加密的字符串(即:盐),目的是提高Token加密的破解难度,注意一定不要泄漏
3、时间戳超时机制
客户端每次请求接口都带上当前时间的时间戳timestamp,服务端接收到timestamp后跟当前时间进行比对,如果时间差大于一定时间(比如:1分钟),则认为该请求失效。时间戳超时机制是防御DOS攻击的有效手段。 例如http://url/getInfo?id=1&timetamp=1661061696
4、URL签名
写过支付宝或微信支付对接的同学肯定对URL签名不陌生,我们只需要将原本发送给server端的明文参数做一下签名,然后在server端用相同的算法再做一次签名,对比两次签名就可以确保对应明文的参数有没有被中间人篡改过。例如http://url/getInfo?id=1&timetamp=1559396263&sign=e10adc3949ba59abbe56e057f20f883e
签名算法过程
-
首先对通信的参数按key进行字母排序放入数组中(一般请求的接口地址也要参与排序和签名,那么需要额外添加
url=http://url/getInfo
这个参数) -
对排序完的数组键值对用&进行连接,形成用于加密的参数字符串
-
在加密的参数字符串前面或者后面加上私钥,然后用md5进行加密,得到sign,然后随着请求接口一起传给服务器。服务器端接收到请求后,用同样的算法获得服务器的sign,对比客户端的sign是否一致,如果一致请求有效
5、防重放
客户端第一次访问时,将签名sign存放到服务器的Redis中,超时时间设定为跟时间戳的超时时间一致,二者时间一致可以保证无论在timestamp限定时间内还是外 URL都只能访问一次,如果被非法者截获,使用同一个URL再次访问,如果发现缓存服务器中已经存在了本次签名,则拒绝服务。
如果在缓存中的签名失效的情况下,有人使用同一个URL再次访问,则会被时间戳超时机制拦截,这就是为什么要求sign的超时时间要设定为跟时间戳的超时时间一致。拒绝重复调用机制确保URL被别人截获了也无法使用(如抓取数据)
方案流程
-
客户端通过用户名密码登录服务器并获取Token;
-
客户端生成时间戳timestamp,并将timestamp作为其中一个参数;
-
客户端将所有的参数,包括Token和timestamp按照自己的签名算法进行排序加密得到签名sign
-
将token、timestamp和sign作为请求时必须携带的参数加在每个请求的URL后边,例:
http://url/request?token=h40adc3949bafjhbbe56e027f20f583a&timetamp=1559396263&sign=e10adc3949ba59abbe56e057f20f883e
-
服务端对token、timestamp和sign进行验证,只有在token有效、timestamp未超时、缓存服务器中不存在sign三种情况同时满足,本次请求才有效;
6、采用HTTPS通信协议
安全套接字层超文本传输协议HTTPS,为了数据传输的安全,HTTPS在HTTP的基础上加入了SSL协议,SSL依靠证书来验证服务器的身份,并为客户端和服务器之间的通信加密。
HTTPS也不是绝对安全的,比如中间人劫持攻击,中间人可以获取到客户端与服务器之间所有的通信内容
总结
自此整个后端接口基本体系就构建完毕了
-
通过Validator + 自动抛出异常来完成了方便的参数校验
-
通过全局异常处理 + 自定义异常完成了异常操作的规范
-
通过数据统一响应完成了响应数据的规范
-
多个方面组装非常优雅的完成了后端接口的协调,让开发人员有更多的经历注重业务逻辑代码,轻松构建后端接口
这里再说几点
-
controller做好try-catch工作,及时捕获异常,可以再次抛出到全局,统一格式返回前端
-
做好日志系统,关键位置一定要有日志
-
做好全局统一返回类,整个项目规范好定义好
-
controller入参字段可以抽象出一个公共基类,在此基础上进行继承扩充
-
controller层做好入参参数校验
-
接口安全验证
相关文章:
规范:前后端接口规范
1、前言 随着互联网的高速发展,前端页面的展示、交互体验越来越灵活、炫丽,响应体验也要求越来越高,后端服务的高并发、高可用、高性能、高扩展等特性的要求也愈加苛刻,从而导致前后端研发各自专注于自己擅长的领域深耕细作。 然…...
Python --NumPy库基础方法(2)
NumPy Numpy(Numerical Python) 是科学计算基础库,提供大量科学计算相关功能,比如数据统计,随机数生成等。其提供最核心类型为多维数组类型(ndarray),支持大量的维度数组与矩阵运算,Numpy支持向…...
音视频入门基础:H.264专题(15)——FFmpeg源码中通过SPS属性获取视频帧率的实现
音视频入门基础:H.264专题系列文章: 音视频入门基础:H.264专题(1)——H.264官方文档下载 音视频入门基础:H.264专题(2)——使用FFmpeg命令生成H.264裸流文件 音视频入门基础&…...
【C++高阶】哈希之美:探索位图与布隆过滤器的应用之旅
📝个人主页🌹:Eternity._ ⏩收录专栏⏪:C “ 登神长阶 ” 🤡往期回顾🤡:模拟实现unordered 的奥秘 🌹🌹期待您的关注 🌹🌹 ❀哈希应用 Ǵ…...
文件包涵条件竞争(ctfshow82)
Web82 利用 session.upload_progress 包含文件漏洞 <!DOCTYPE html> <html> <body> <form action"https://09558c1b-9569-4abd-bf78-86c4a6cb6608.challenge.ctf.show//" method"POST" enctype"multipart/form-data"> …...
通信原理-思科实验三:无线局域网实验
实验三 无线局域网实验 一:无线局域网基础服务集 实验步骤: 进入物理工作区,导航选择 城市家园; 选择设备 AP0,并分别选择Laptop0、Laptop1放在APO范围外区域 修改笔记本的网卡,从以太网卡切换到无线网卡WPC300N 切…...
*算法训练(leetcode)第三十一天 | 1049. 最后一块石头的重量 II、494. 目标和、474. 一和零
刷题记录 *1049. 最后一块石头的重量 II*494. 目标和474. 一和零 *1049. 最后一块石头的重量 II leetcode题目地址 本题与分割等和子集类似,要达到碰撞最后的石头重量最小,则尽可能把石头等分为两堆。 时间复杂度: O ( m ∗ n ) O(m * n)…...
mac中如何使用obs推流以及使用vlc播放
使用obs推流 1.打开obs,在“来源”框中->点加号->选择媒体源->选择本地ts文件 2.obs中->点击右下角设置->点直播->服务选自定义->服务器填写你的srt服务url,比如:srt://192.168.13.211:14000?modecaller 注意ÿ…...
shopee虾皮 java后端 一面面经 整体感觉不难
面试总结:总体不难,算法题脑抽了只过了一半,面试官点出了问题说时间到了,反问一点点,感觉五五开,许愿一个二面 1.Java中的锁机制,什么是可重入锁 Java中的机制主要包括 synchronized关键字 Loc…...
HydraRPC: RPC in the CXL Era——论文阅读
ATC 2024 Paper CXL论文阅读笔记整理 问题 远程过程调用(RPC)是分布式系统中的一项基本技术,它允许函数在远程服务器上通过本地调用执行来促进网络通信,隐藏底层通信过程的复杂性简化了客户端/服务器交互[15]。RPC已成为数据中心…...
pve笔记
配置显卡直通参考 https://blog.csdn.net/m0_59148723/article/details/130923893 https://foxi.buduanwang.vip/virtualization/pve/561.html/ https://www.cnblogs.com/MAENESA/p/18005241 https://www.wangsansan.com/archives/181/ pve配置显卡直通到虚拟机后,…...
typecho仿某度响应式主题Xaink
新闻类型博客主题,简洁好看,适合资讯类、快讯类、新闻类博客建站,响应式设计,支持明亮和黑暗模式 直接下载 zip 源码->解压后移动到 Typecho 主题目录->改名为xaink->启用。 演示图: 下载链接: t…...
springcloud RocketMQ 客户端是怎么走到消费业务逻辑的 - debug step by step
springcloud RocketMQ ,一个mq消息发送后,客户端是怎么一步步拿到消息去消费的?我们要从代码层面探究这个问题。 找的流程图,有待考究。 以下我们开始debug: 拉取数据的线程: PullMessageService.java 本…...
GPT-4o mini小型模型具备卓越的文本智能和多模态推理能力
GPT-4o mini 是首个应用OpenAI 指令层次结构方法的模型,这有助于增强模型抵抗越狱、提示注入和系统提示提取的能力。这使得模型的响应更加可靠,并有助于在大规模应用中更安全地使用。 GPT-4o mini 在学术基准测试中,无论是在文本智能还是多模…...
Milvus 向量数据库进阶系列丨部署形态选型
本系列文章介绍 在和社区小伙伴们交流的过程中,我们发现大家最关心的问题从来不是某个具体的功能如何使用,而是面对一个具体的实战场景时,如何选择合适的向量数据库解决方案或最优的功能组合。在 “Milvus 向量数据库进阶” 这个系列文章中&…...
【React】详解受控表单绑定
文章目录 一、受控组件的基本概念1. 什么是受控组件?2. 受控组件的优势3. 基本示例导入和初始化定义函数组件处理输入变化处理表单提交渲染表单导出组件 二、受控组件的进阶用法1. 多个输入框的处理使用多个状态变量使用一个对象管理状态 2. 处理选择框(…...
使用puma部署ruby on rails的记录
之前写过一篇《记录一下我的Ruby On Rails的systemd服务脚本》的记录,现在补上一个比较政治正确的Ruby On Rails的生产环境部署记录。使用Puma部署项目。 创建文件 /usr/lib/systemd/system/puma.service [Unit] DescriptionPuma HTTP Server DocumentationRuby O…...
如何在Linux上使用Ansible自动化部署
Ansible是一个开源的自动化工具,可以帮助开发人员和系统管理员对大规模的服务器进行自动化部署和管理。它使用SSH协议来在远程服务器上执行任务,并通过模块化的方式提供了丰富的功能,可以轻松地管理服务器配置、软件部署和应用程序运行。 在…...
scrapy爬取城市天气数据
scrapy爬取城市天气数据 一、创建scrapy项目二、修改settings,设置UA,开启管道三、编写爬虫文件四、编写items.py五、在weather.py中导入WeatherSpiderItem类六、管道中存入数据,保存至csv文件七、完整代码一、创建scrapy项目 先来看一下爬取的字段情况: 本次爬取城市天…...
一天搞定React(5)——ReactRouter(下)【已完结】
Hello!大家好,今天带来的是React前端JS库的学习,课程来自黑马的往期课程,具体连接地址我也没有找到,大家可以广搜巡查一下,但是总体来说,这套课程教学质量非常高,每个知识点都有一个…...
微信小程序之计算器
在日常生活中,计算器是人们广泛使用的工具,可以帮助我们快速且方便地计算金额、成本、利润等。下面将会讲解如何开发一个“计算器”微信小程序。 一、开发思路 1、界面和功能 “计算器”微信小程序的页面效果如图所示 在计算器中可以进行整数和小数的…...
【logstash】logstash使用多个子配置文件
这里有个误区在pipelines.yml中写conf.d/*,实测会有问题,不同的filter处理逻辑会复用。 现在有两个从kafka采集日志的配置文件:from_kafka1.conf,from_kafka2.conf 修改pipelines.yml配置文件 config/pipelines.yml- pipeline.i…...
暴风骑士S9电摩上市,定义青少年骑行安全新标准
暴风骑士,作为全球高端儿童电动车的开创品牌,以其卓越的技术实力和创新精神,不断推动行业发展。如今,暴风骑士再次突破自我,推出了全新力作——S9青少年电摩。这款全新上市的青少年专属电摩,以其领先的安全…...
spring security如何适配盐存在数据库中的密码
19.token认证过滤器代码实现_哔哩哔哩_bilibili19.token认证过滤器代码实现是SpringSecurity框架教程-Spring SecurityJWT实现项目级前端分离认证授权-挑战黑马&尚硅谷的第20集视频,该合集共计41集,视频收藏或关注UP主,及时了解更多相关视…...
Go语言编程 学习笔记整理 第2章 顺序编程 后半部分
1.流程控制 1.1 条件语句 if a < 5 { return 0 } else { return 1 } 注意:在有返回值的函数中,不允许将“最终的”return语句包含在if...else...结构中, 否则会编译失败!!! func example(x int) i…...
美团后端二面
美团后端二面 ……………………………… 两道场景 一道 数字转中文读法(1000-》一千) 0八股0自我介绍 反问 “您觉得我能过吗?” “这个需要横行对比之后才能有结果” ……………………………… 什么时候到岗 场景题 1 假设我有一个…...
学懂C语言(十六):对C语言作用域规则 局部变量、全局变量的认识
一、C 作用域规则 任何一种编程中,作用域是程序中定义的变量所存在的区域,超过该区域变量就不能被访问。C 语言中有三个地方可以声明变量: 局部变量:在函数或块内部全局变量:在所有函数外部形式参数:在函数…...
关于TS(typescript)的理论知识
关于TS(typescript)的理论知识 TypeScript 是一种由微软开发的开源编程语言,它是 JavaScript 的一个超集,添加了可选的静态类型和基于类的面向对象编程。TypeScript 最终会被编译成纯 JavaScript 代码,以便在任何支持 …...
【OpenCV C++20 学习笔记】基本图像容器——Mat
【OpenCV C20 学习笔记】基本图像容器——Mat 概述Mat内部结构引用计数机制颜色数据格式 显式创建Mat对象使用cv::Mat::Mat构造函数矩阵的数据项 使用数组进行初始化的构造函数cv::Mat::create函数MATLAB风格的初始化小型矩阵通过复制创建Mat对象 Mat对象的输出其他普通数据项的…...
枚举单例是怎么保证线程安全和防止反射的
枚举单例在Java中具有天然的线程安全性和防止反射攻击的特性,这是由于Java对枚举类型的特殊处理方式。以下是详细解释: 1. 线程安全性 Java 枚举类的特性 类加载机制:枚举类型在Java中是特殊的类,由JVM保证其线程安全性。枚举类…...
传知代码-智慧医疗:纹理特征VS卷积特征(论文复现)
代码以及视频讲解 本文所涉及所有资源均在传知代码平台可获取 论文链接:https://www.sciencedirect.com/science/article/abs/pii/S1076633223003537?__cf_chl_rt_tkJ9Aipfxyk5d.leu48P20ePFNd4B2aunaSmzVpXCg.7g-1721292386-0.0.1.1-6249 论文概述 今天我们把视线…...
数据结构中的八大金刚--------八大排序算法
目录 引言 一:InsertSort(直接插入排序) 二:ShellSort(希尔排序) 三:BubbleSort(冒泡排序) 四: HeapSort(堆排序) 五:SelectSort(直接选择排序) 六:QuickSort(快速排序) 1.Hoare版本 2.前后指针版本 …...
ACC2.【C语言】经验积累 栈区简单剖析
int main() {int i0;int arr[10]{1,2,3,4,5,6,7,8,9,10};for (i0;i<12;i){arr[i]0;printf("A");}return 0; } 执行后无限打印A 在VS2022,X86,Debug环境下,用监视后,原因是arr[12]的地址与i的地址重合(数组越界&…...
c# 索引器
索引器(Indexer)允许你像访问数组一样,通过索引访问对象的属性或数据。索引器的主要用途是在对象内部封装复杂的数据结构,使得数据访问更加直观。下面是关于 C# 索引器的详细解释及示例: 基本语法 索引器的语法类似于…...
低代码如何加速数字化转型
数字化转型,正日益决定企业成功的关键。这里的一个关键因素是它可以以更快的速度和质量来实施技术计划。在当今瞬息万变的商业环境中,战略性地采用低代码平台对于旨在加快上市时间、增强业务敏捷性和促进跨团队无缝协作的首席技术官来说至关重要。日益增…...
Pytest进阶之fixture的使用(超详细)
目录 Fixture定义 Fixture使用方式 作为参数使用 Fixture间相互调用(作为参数调用) 作为conftest.py文件传入 Fixture作用范围Scope function class module session Fixture中params和ids Fixture中autouse Fixture中Name 总结 pytest fixture 是一种用来管理测试…...
GitHub 详解教程
1. 引言 GitHub 是一个用于版本控制和协作的代码托管平台,基于 Git 构建。它提供了强大的功能,使开发者可以轻松管理代码、追踪问题、进行代码审查和协作开发。 2. Git 与 GitHub 的区别 Git 是一个分布式版本控制系统,用于跟踪文件的更改…...
边界网关IPSEC VPN实验
拓扑: 实验要求:通过IPSEC VPN能够使PC2通过网络访问PC3 将整个路线分为三段 IPSEC配置在FW1和FW2上,在FW1与FW2之间建立隧道,能够传递IKE(UDP500)和ESP数据包,然后在FW1与PC2之间能够流通数据…...
力扣高频SQL 50题(基础版)第六题
文章目录 1378. 使用唯一标识码替换员工ID题目说明思路分析实现过程结果截图总结 1378. 使用唯一标识码替换员工ID 题目说明 Employees 表: ---------------------- | Column Name | Type | ---------------------- | id | int | | name | varchar | ------…...
在一个事物方法中开启新事物,完成对数据库的修改
在Java中,使用Transactional注解来管理事务非常常见。但是,在一个已经标记为Transactional的方法内部调用另一个也标记了Transactional的方法时,如果不正确处理,可能会导致一些意料之外的行为。这是因为默认情况下,Spr…...
ffmpeg的vignetting filter
vignetting filter是暗角过滤器 vignetting filter在官网是vignette。但是我查了一下,vignetting应该是正确的表达,vignette是什么鬼? 官网参数 官书参数 参数解释 angle,x0,y0可以使用表达式。 angle:不知道什么意思…...
商场导航系统:从电子地图到AR导航,提升顾客体验与运营效率的智能解决方案
商场是集娱乐、休闲、社交于一体的综合性消费空间,随着商场规模的不断扩大和布局的日益复杂,顾客在享受丰富选择的同时,也面临着寻路难、店铺曝光率低以及商场管理效率低下等挑战。商场导航系统作为提升购物体验的关键因素,其重要…...
vue3中父子组件的双向绑定defineModel详细使用方法
文章目录 一、defineProps() 和 defineEmits()二、defineModel() 的双向绑定2.1、基础示例2.2、定义类型2.3、声明prop名称2.4、其他声明2.5、绑定多个值2.6、修饰符和转换器2.7、修饰符串联 一、defineProps() 和 defineEmits() 组件之间通讯,通过 props 和 emits…...
耳机、音响UWB传输数据模组,飞睿智能低延迟、高速率超宽带uwb模块技术音频应用
在数字化浪潮席卷全球的今天,无线通信技术日新月异,其中超宽带(Ultra-Wideband,简称UWB)技术以其独特的优势,正逐步成为无线传输领域的新星。本文将深入探讨飞睿智能UWB传输数据模组在音频应用中的创新应用…...
webpack配置报错:Invalid options object.
前言: 今天在使用webpack进行项目配置的时候,运行之后终端报错:Invalid options object. Dev Server has been initialized using an options object that does not match the API schema. - options has an unknown property inline. Thes…...
Java 并发编程:一文了解 Java 内存模型(处理器优化、指令重排序与内存屏障的深层解析)
大家好,我是栗筝i,这篇文章是我的 “栗筝i 的 Java 技术栈” 专栏的第 022 篇文章,在 “栗筝i 的 Java 技术栈” 这个专栏中我会持续为大家更新 Java 技术相关全套技术栈内容。专栏的主要目标是已经有一定 Java 开发经验,并希望进…...
谷粒商城实战笔记-64-商品服务-API-品牌管理-OSS前后联调测试上传
文章目录 1,拷贝文件到前端工程2,局部修改3,在品牌编辑界面使用上传组件4,OSS配置允许跨域5,测试multiUpload.vue完整代码singleUpload.vue完整代码policy.js代码 在Web应用开发中,文件上传是一项非常常见的…...
Springboot 开发之 RestTemplate 简介
一、什么是RestTemplate RestTemplate 是Spring框架提供的一个用于应用中调用REST服务的类。它简化了与HTTP服务的通信,统一了RESTFul的标准,并封装了HTTP连接,我们只需要传入URL及其返回值类型即可。RestTemplate的设计原则与许多其他Sprin…...
Django transaction.atomic()事务处理
在Django中,transaction.atomic()是一个上下文管理器,它会自动开始一个事务,并在代码块执行完毕后提交事务。如果在代码块中抛出异常,事务将被自动回滚,确保数据库的一致性和完整性。 在实际应用中,你可能需…...
2024.07-电视版免费影视App推荐和猫影视catvod、TVBox源(最新接口地址)
文章目录 电视版免费影视App推荐精选列表(2024.07可用筛选列表):2024.07可用筛选列表,盲盒资源打包合集下载安装说明真的是盲盒? 猫影视catvod、TVBoxTVBox源推荐可用列表目前不可用列表(前缀为错误状态码&…...