SpringBoot
SpringBoot开发技术 — 过滤器、拦截器、SpringBoot事件
技术无止境,唯有继续沉淀…
SpringBoot过滤器
实际开发中:比如统计在线的用户,敏感词过滤或者基于URL进行访问控制;这些需求的共同点就是—每个接口请求的时候都会进行该类操作
SpringBoot中使用过滤器就实现Filter接口即可,重写doFilter接口【和Servlet时代相同】
服务发现机制: SpringBoot要能够发现,使用主类的注解:@ServletComponentScan主类
过滤器其实就是之前的Servlet规范中的概念,具体的功能实现是Tomcat提供,过滤器就是对资源的请求和响应的过滤【之前用作字符过滤器】,自身不会产生响应
这里可以在blog-demo中进行演示,创建一个SecretController存放受保护的内容
@RestController
@RequestMapping("/secret")
public class SecretController {
@GetMapping
public String secret() {
return "this content is secret, I'm Cfeng";
}
}
之后可以再此基础上创建一个SessionController用来进行身份的认证
@RestController
@RequestMapping("/session")
public class SessionController {
@PostMapping
public String doLogin(HttpServletResponse response ,@RequestBody SessionQuery sessionQuery) {
if(authenticate(sessionQuery)) {
certificate(response);
return "success";
}
return "failed";
}
private boolean authenticate(SessionQuery sessionQuery) {
return Objects.equals(sessionQuery.getUsername(),"admin") && Objects.equals(sessionQuery.getPassword(),"123456");
}
private void certificate(HttpServletResponse response) {
Cookie credential = new Cookie("sessionId","test-token");
response.addCookie(credential);
}
之后编写过滤器进行过滤,过滤器的WebFilter注解就可以设置需要过滤的所有的url,这里的过滤就判断是否有Session即可
@Slf4j
@WebFilter(urlPatterns = "/secret/*")
public class SessionFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
Cookie[] cookies = Optional.ofNullable(((HttpServletRequest)servletRequest).getCookies()).orElse(new Cookie[0]);
boolean unauthorized = true;
for(Cookie cookie : cookies) {
if("sessionId".equals(cookie.getName()) && "test-token".equals(cookie.getValue())) {
unauthorized = false;
}
}
if(unauthorized) {
log.error("UNAUTHORIZED");
unauthorizedResp(servletResponse);
}else {
filterChain.doFilter(servletRequest,servletResponse);
}
}
private void unauthorizedResp(ServletResponse response) throws IOException {
HttpServletResponse httpServletResponse = (HttpServletResponse)response;
httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
httpServletResponse.setHeader("content-type","text/html;charset=UTF-8");
httpServletResponse.setCharacterEncoding("UTF-8");
PrintWriter writer = httpServletResponse.getWriter();
writer.write("Cfeng tell you : UNAUTHORIZED");
writer.flush();
writer.close();
}
}
@Slf4j的作用就是日志的输出,一键创建日志记录器logger
这里相当于是注册了一个服务,那么接下来就是启用过滤器,在Spring时代就是需要进行配置,SpringBoot时代,就需要使用==@ServletComponentScan==,启用用@WebFilter修饰的过滤器
SpringBoot拦截器
拦截器Interceptor由Spring提供,Interceptor和Filter是类似的,但是操作的粒度更小,整体功能没有FIlter强大,支持自定义预处理preHanle和后续处理postHandle,使用拦截器的前提是需要实现HandlerInterceptor接口
- preHandle: 执行实际的处理程序之前调用,还没有生成视图
- postHandle: 处理程序之后调用
- afterCompletion: 请求已经响应,并且视图生成完毕
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fln5fKhT-1656353533802)(https://resource.shangmayuan.com/droxy-blog/2020/11/30/d8eecd13a7834bc4b5fd628a4f239fea-1.jpg)]
可以看到执行的事件是不相同的,过滤器过滤之后才会给到DispatcherServlet,在执行具体的处理器方法的时候会调用相关的拦截器; 这里的拦截器应该是一个HandlerExecutionChain ---- 也就是说这里是多个拦截器进行处理【后面Security会分享】
这里we先创建一个拦截器,继承HanderIntercpetorAdapter【实现了接口】,这样就可以只用重写几个方法即可,HanderIntercpetorAdapter过时了,所以还是直接实现接口
还是需要使用日志打印请求的参数,所以需要@Slf4j
@Slf4j
@Component
public class LogRequestInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info(String.format("[preHandle] [%s] [%s]%s%s",request,request.getMethod(),request.getRequestURI(),getParameters(request)));
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("[postHandle]");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
if(ex != null) {
ex.printStackTrace();
}
log.info(String.format("[afterCompletion][%s][%exception:%s]",request,ex));
}
private String getParameters(HttpServletRequest request) {
StringBuilder parameterBuilder = new StringBuilder();
Enumeration<String> names = request.getParameterNames();
if(names != null) {
parameterBuilder.append("?");
while (names.hasMoreElements()) {
if(parameterBuilder.length() > 1) {
parameterBuilder.append("&");
}
String pointer = names.nextElement();
parameterBuilder.append(pointer).append("=").append(request.getParameter(pointer));
}
}
return parameterBuilder.toString();
}
}
在springBoot中就直接将拦截器创建Bean实例放入容器,所以加上@Component注解;
SpirngBoot时代要注册拦截器不同于xml,而是使用的配置类
@Configuration
@RequiredArgsConstructor
public class InterceptorConfig implements WebMvcConfigurer {
private final LogRequestInterceptor logRequestInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(logRequestInterceptor).addPathPatterns("/**");
}
}
过滤器的过滤的路径直接在注解位置进行声明,而拦截器的拦截的路径则是在webMvcConfigurer中的addInterceptors方法中进行addPathPatterns来设置
SpirngBoot事件
程序都是要求松耦合的,当模块耦合严重的时候,就需要解耦,像Spirng的IOC和AOP都是可以解耦的,“事件驱动”也是解耦的重要的手段,【事件驱动之前的GUI使用多,前面的博客提过】,springBoot也有一套事件驱动机制
事件驱动模型和消息mq是相同的,就是一种服务发现的机制,将相关的EventSource绑定给Event,同时使用EventListener进行监听,触发时执行操作
- EventSource: 事件源:事件发生的场所,也就是监控的对象,比如容器等
- Event: 事件,对事件信息进行封装,就是一种通知
- EventListener: 事件监听器,监听事件,对其做出反应
当业务领域有状态变化的时候,就会发送消息通知其他的模块,事件源和监听器之间的代码没有 ----直接调用的关系
松耦合也会造成问题,就是没有流程的显式描述,所以业务流程就很复杂,不方便调试和修改
内置事件
Spring内置了5中标准上下文事件和四种ApplicationContext事件,这些事件在框架内部本身就是大量使用的
- ContextRefreshedEvent: 上下文更新事件。也就是ApplicaitonContext初始化或者更新时触发,也会在ConfigurableApplicationContext接口的refresh方法触发 【容器初始化更新】
- ContextStrartedEvent: 上下文开始事件,开始、重启容器,或者调用ConfigurableApplicationContext的start方法触发
- ContextStoppedEvent: 上下文停止事件,容器停止或者调用stop方法时触发
- ContextClosedEvent: 上下文关闭事件,也就是容器关闭的时候触发,会销毁所有的bean【singleton】
- RequestHandledEvent:请求处理事件,一个http请求结束的时候触发事件
- ApplicaitonStartedEvent: 应用启动事件,SpirngBoot应用启动时触发该事件
- ApplicationEvitonmentPreparedEvent: 应用环境就绪事件,就是一个boot应用环境就绪,但是上下文还未就绪的时候触发
- ApplicaitonPreparedEvent: 应用就绪事件: 应用的环境和上下文都加载完,但是Bean还未加载完成的时候触发
- ApplicationFailedEvent: 应用异常事件,启动出现异常的时候会触发
这种事件的松耦合,类似之前的SpringBoot的自动装配,只要相关的starter实现了相关的bean,并且注册到META-INFO/spring.factories中就可以被SpringBoot自动加载【当然时按需加载,相关的OnConditional】
监听内置事件
程序中监听内置事件只需要创建事件对应的监听器,并且注册即可【Servlet时代也有监听器】,比如这里监听应用就绪但是Bean未加载完成事件
创建监听器,需要在应用的主启动类中进行注册,监听器的注册使用的时SpringApplication对象,对象的addListener方法进行,而SpirngBoot容器就是应用对象的run方法产生的
监听器全部放在event包下面
@Slf4j
public class CustomApplicationPreparedListener implements ApplicationListener<ApplicationPreparedEvent> {
@Override
public void onApplicationEvent(ApplicationPreparedEvent event) {
log.info("Cfeng, the applicationPreparedEvent");
}
}
监听器有了,要能够使用监听器,需要主类发现,【拦截器注册之后主类会自动去查询WebMvcConfigure类】— 所以主类可以查询的,其他的诸如过滤器,监听器,配置类都需要发现 ,还有Mybatis的mapper也是需要发现的,但是JPA只要在主类包下面,就可以自动扫描Entity和Repository,不在的也需要相关的scan
@SpringBootApplication
@EnableConfigurationProperties({BlogProperties.class, FileStorageProperties.class})
@ServletComponentScan
public class BlogApplication {
public static void main(String[] args) {
SpringApplication application = new SpringApplication(BlogApplication.class);
application.addListeners(new CustomApplicationPreparedListener());
application.run(args);
}
}
自定义事件
自定义事件进行代码解耦,要使用自定义事件,需要事件,事件源和事件监听器
- 创建自定义事件类Event,继承ApplicationEvent类,
@Getter
public class MessageEvent extends ApplicationEvent {
private final String message;
public MessageEvent(Object source, String message) {
super(source);
this.message = message;
}
}
这里就实现了ApplicationEvent,那么这个MessageEvent就可以作为事件服务
@Slf4j
@RequiredArgsConstructor
@Component
public class MessageEventPublisher {
private final ApplicationEventPublisher applicationEventPublisher;
public void publishEvent(String message) {
log.info("publish an event.Message:" + message);
applicationEventPublisher.publishEvent(new MessageEvent(this,message));
}
}
- 创建事件监听器,创建事件监听器有两种范式,一种就是上面的内置事件监听的方式,使用ApplicationListener
@Slf4j
@Component
public class MessageEventListener implements ApplicationListener<MessageEvent> {
@Override
public void onApplicationEvent(MessageEvent event) {
log.info("Other business...Message" + event.getMessage());
}
}
除了实现Listener接口之外,还可以使用==@EventListener==, 这个注解放在方法上面,代表该方法处理相关的事件,这个类也就是一个监听器类
接下来测试一下这个自定义事件
@SpringBootTest(classes = {BlogApplication.class},webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class EventTests {
@Resource
MessageEventPublisher publisher;
@Test
public void publishAnEvent_thenCheckConsole() {
publisher.publishEvent("Cfeng 自定义事件");
}
}
可以查看结果
2022-06-14 21:03:12,841 INFO [main] indv.cfeng.event.MessageEventPublisher: publish an event.Message:Cfeng 自定义事件
2022-06-14 21:03:12,843 INFO [main] indv.cfeng.event.MessageEventListener: Other business...MessageCfeng 自定义事件
异步事件
在默认情况下,事件的发布与监听是同步执行的。当要用到异步事件时,需要进行额外的支付。具体方式在创建ApplicaitonEventMuliticaster的JavaBean
比如这里就是config中创建AsynchronousEventConfig
@Configuration
public class AsynchronousEventConfig {
@Bean(name = "applicaitonEventMulticaster")
public ApplicationEventMulticaster simpleApplicationEventMulticaster() {
SimpleApplicationEventMulticaster eventMulticaster = new SimpleApplicationEventMulticaster();
eventMulticaster.setTaskExecutor(new SimpleAsyncTaskExecutor());
return eventMulticaster;
}
}
这里相当于是创建一个异步的Bean,异步事件的主要依靠就是ApplicationEventMulticaster接口,创建的就是下面的一个简单实现类Simple~
Controller开发策略 — Optional,ResponseEntity
其实对于java8中的包裹类的使用,JPA本身就是在运用,比如CrudRepository就是
Optional<T> findById(ID id);
所以find方法返回的都是一个Optional包裹的对象,所以we自己开发repository的时候有必要借鉴其做法,这样可以避免空指针异常,Optional中的map方法可以进行映射操作; 就是对于find的所有的结果,使用箭头函数进行操作; 最好配合OrElse一起使用,如果为空,那么就进行OrElse中的操作
而获取一个对象的所有的非空属性,需要使用到BeanWrapper,利用Wrapper包裹source,之后及那个其转化为Stream流,map映射之后,进行filter,之后collect形成List
private List<String> getNullProperties(Object source) {
final BeanWrapper wrapperSource = new BeanWrapperImpl(source);
return Stream.of(wrapperSource.getPropertyDescriptors())
.map(FeatureDescriptor::getName)
.filter(propertyName -> Objects.isNull(wrapperSource.getPropertyValue(propertyName)))
.collect(Collectors.toList());
}
- Optional类的使用: 为了避免查询结果出现空的情况【程序报错空指针】所以需要进行判空,不要使用 == null; 而是直接使用java8提供的方式,先封装为Optional<类>, 之后使用.isPresent方法判断是否存在,如果不存在,就直接…,否则就是用封装对象的get方法获得包装之前的对象
- ReponseEntity的使用: Rest风格的Controller都是返回的响应体,对应的就是ReponseEntity,所以每一个返回结果都可以是ReponseEntity,其包装的就是之前想要放回的数据,好处就是其return的时候需要指定HttpStatus,也就是状态码,符合前后端交互的需求
- Objects.equals(xxx,yyy)的运用,使用该方法可以很好的比较二者如字符串的内容是否相等,比直接使用equals的好处就是避免了空指针异常
SpringBoot访问static中的静态资源
需要注意的是springBoot进行了自动配置,也就是说会自动扫描一些文件夹:classpath:/META-INF/resources/,classpath:/resources/,classpath:/static/,classpath:/public/; 也就是会自动查询这些后缀之下的静态资源
在templates下面填写资源的路径的时候一定要加上项目的根路径,如果是./等都是在当前的路径作为相对路径,所以一般使用绝对路径
<script src="/restDemo/js/JQuery.js"></script>
<img src="/restDemo/img/test.png">
自动配置之后,就不需要再写staic,resources等中间路径了,相当于会自动查找restDemo项目下面的static文件夹
RESTful web服务
随着移动互联网的发展,Web的更迭,前后端分离的软件设计架构在Web,前后端分离的趋势很明显,HTTP规范指定人制定了REST规范,也就是URL用以定位资源,HTTP动词可以描述曹祖。SpringBoot提供了REST相关支持
HTTP动词
之前就使用果Restful风格,在了解了Vue之后确实感受到了前后端交互使用遵守REST规范的强大,REST构建后端的关键点就是使用URL和HTTP动词来描述调用方和资源的交互
- GET: 从服务端获取资源
- POST: 向服务器上传资源,新建资源
- PUT: 在服务端更新资源,客户端会提供更改后完整的资源
- PATCH: 服务端更新资源,强调是在客户端提供改变的属性
- DELETE: 从服务端删除资源
使用HTTP动词,结合合适的URL路径和路径遍历,基本上可以覆盖各种操作;需要注意的是一定要保证唯一性: HTTP动词 + ur
比如按照名称查询姓名和按照ID查询姓名,如果按照RESTful风格,就是GetMapping(/student/{stuName}), GetMaping(/student/{stuId}),前台传入的Restful风格的URL是不知道走哪个处理器的
这里可以举几个普通的Restful风格的例子、
- GET /vehicle/list 获取Vehicle记录的列表
- GET /vechicle/{id} 根据id获取Vechicle的信息
- Post/vehicle: 新建、上传新的Vehicle记录
- PUT /vehicle/{id} : 替换某个id的vehicle的信息
- PATCH/vehicle/{id} : 修改Vehicle记录的某个片段
- DELETE /vehicle/{id} : 删除对应的Vehicle记录
构建一个RestDemo
这里构建一个Vehicle的Demo实现上面的接口,上面的就是前后端交互的接口了
@Entity
@Table(name = "t_vehicle")
@Accessors(chain = true)
@Getter
@Setter
@ToString
public class Vehicle {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String description;
}
这里测试就创建一个单表即可
接下来就是创建repository来操作数据库
public interface VehicleRepository extends JpaRepository<Vehicle,Long> {
}
之后的Service因为太简单不创建了,直接用Controller调用Repository即可
@RestController
@RequestMapping("/api/vehicle")
@RequiredArgsConstructor
public class VehicleController {
private final VehicleRepository vehicleRepository;
@GetMapping("/list")
public ResponseEntity<List<Vehicle>> getVehicleList() {
return new ResponseEntity<>(vehicleRepository.findAll(), HttpStatus.OK);
}
@GetMapping("/{id}")
public ResponseEntity<Vehicle> getVehicleById(@PathVariable Long id) {
return vehicleRepository.findById(id).map(vehicle -> new ResponseEntity<>(vehicle,HttpStatus.OK)).orElse(new ResponseEntity<>(null,HttpStatus.BAD_REQUEST));
}
@PostMapping("/")
public ResponseEntity<Vehicle> addVehicle(@RequestBody Vehicle vehicle) {
return new ResponseEntity<>(vehicleRepository.save(vehicle),HttpStatus.OK);
}
@PutMapping("/")
public ResponseEntity<Vehicle> replaceVehicle(@RequestBody Vehicle vehicle) {
Optional<Vehicle> oldVehicle = vehicleRepository.findById(vehicle.getId());
if(!oldVehicle.isPresent()) {
return new ResponseEntity<>(null,HttpStatus.BAD_REQUEST);
}
return new ResponseEntity<>(vehicleRepository.save(vehicle),HttpStatus.OK);
}
@PatchMapping("/")
public ResponseEntity<Vehicle> modifyVehicle(@RequestBody Vehicle vehicle) {
Optional<Vehicle> findById = vehicleRepository.findById(vehicle.getId());
Vehicle oldOne;
if(!findById.isPresent()) {
return new ResponseEntity<>(null,HttpStatus.BAD_REQUEST);
} else {
oldOne = findById.get();
}
Vehicle newOne = new Vehicle();
List<String> nullProperties = this.getNullProperties(oldOne);
BeanUtils.copyProperties(newOne,oldOne,nullProperties.toArray(new String[0]));
return new ResponseEntity<>(vehicleRepository.save(newOne),HttpStatus.OK)
}
@DeleteMapping("/{id}")
public ResponseEntity<Vehicle> deleteVehicle(@PathVariable Long id) {
vehicleRepository.deleteById(id);
return new ResponseEntity<>(null,HttpStatus.OK);
}
private List<String> getNullProperties(Object source) {
final BeanWrapper wrapperSource = new BeanWrapperImpl(source);
return Stream.of(wrapperSource.getPropertyDescriptors())
.map(FeatureDescriptor::getName)
.filter(propertyName -> Objects.isNull(wrapperSource.getPropertyValue(propertyName)))
.collect(Collectors.toList());
}
}
从这里开始不使用h2了,直接使用Mysql,mysql加入也是直接加入connecter依赖,之后就是在yml中配置数据源,配置hibernate的连接的策略,配置连接池
这里可以测试其中的Post请求的方法,首先建立一个简单的表单提交一个Vehicle对象
{{> header}}
<h1>Welcome to test the RestDemo</h1>
<form>
汽车编号<input type="text" name="id"/><br>
汽车名称<input type="text" name="veName"><br>
汽车描述<input type="text" name="description"><br>
<input type="submit" value="注册">
</form>
<script src="/restDemo/js/JQuery.js"></script>
<script type="text/javascript">
$("form").submit(function () {
let formObj = {};
let formArray = $("form").serializeArray();
$.each(formArray,function (i,item) {
formObj[item.name] = item.value;
});
$.ajax({
type: 'POST',
url: "/restDemo/api/vehicle/",
data: JSON.stringify(formObj),
contentType: 'application/json',
success: function () {
alert(data);
}
})
})
</script>
{{> footer}}
这里最主要的就是将前台的表单提交的数据进行JSON格式化,直接使用Jquery的submit来进行请求的提交,首先就是将数据封装为JSON对象,这里就是使用serilizeArray获得表单元素组成的一个JSON对象,这里之后要对每一个表单元素的name和value取出,放入formObj中构成一个JSON对象,传输的时候使用JSON.stringify将formObj这个JSON对象转换为JSON字符串,从而就可以正确被后台进行@RequestBody进行注入
这里再提交表单之后,就会访问上面的POST方法,然后将数据插入数据库中
Hibernate: select vehicle0_.id as id1_0_0_, vehicle0_.description as descript2_0_0_, vehicle0_.ve_name as ve_name3_0_0_ from t_vehicle vehicle0_ where vehicle0_.id=?
Hibernate: select next_val as id_val from hibernate_sequence for update
Hibernate: update hibernate_sequence set next_val= ? where next_val=?
Hibernate: insert into t_vehicle (description, ve_name, id) values (?, ?, ?)
这里就可以在数据库中查询到这个数据
JPA整合Mysql
@Entity注解如果在数据库中已经存在表就会修改表的结构,如果不存在就会创建表结构,相关的具体配置包括spring的datasource配置数据源,jpa配置jpa下属相关的操作【hibernate配置相关的策略】
spring:
datasource:
url: jdbc:mysql://localhost:3306/cfengrest?servertimezone=GMT%2B8
username: cfeng
password: XXXX
driver-class-name: com.mysql.cj.jdbc.Driver
dbcp2:
initial-size: 10
min-idle: 10
max-idle:50
max-wait-millis: 3000
time-between-eviction-runs-millis: 200000
remove-abandoned-on-maintenance: 2000000
jpa:
database: mysql
show-sql: true
hibernate:
ddl-auto: update
naming:
strategy: org.springframework.boot.orm.jpa.hibernate.SpringPhyicalNamingStategy
这样就可以成功连接mysql了,JPA可以自动更新表结构,如果没有创建相关的表,JPA会进行自动的创建🔮
GetMapping和RequestMapping
Controller上面加上RestController注解之后就相当于给每一个处理器方法都加上一个@ReponseBody注解,所以这样子之后就不能进行视图的转发,这里要解决这个问题就直接发起get请求之后,返回值为ModelAndView,return一个实例对象,属性ViewName就是转发的视图
其实GetMapping等就是RequestMapping
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RequestMapping(
method = {RequestMethod.PUT}
)
public @interface PutMapping {
.......
@AliasFor(
annotation = RequestMapping.class
)
String[] path() default {};
@AliasFor(
annotation = RequestMapping.class
)
String[] params() default {};
@AliasFor(
annotation = RequestMapping.class
)
String[] headers() default {};
@AliasFor(
annotation = RequestMapping.class
)
String[] consumes() default {};
........
可以看到就是ReqeustMapping制定了请求的方式为PUT,POST等
下面指定了很多属性,可以简单查看几个常用的属性
- path: 指定接口的url访问路径,这个是默认的
- params: 指定请求中必须包含的参数
- headers: 指定请求中必须包含的请求头
- consumes: 指定请求的内容类型,Content-type
- produces: 指定响应的内容类型,也就是Content-type,比如text/html等
这里可以借助params指定一个必须的参数,这样请求必须携带这个参数,不然就是BAD_REQUEST
需求: 需要在查询一条Vehicle记录的时候,将这条记录的信息复制再次插入数据库中,这个时候单纯依靠Http动词不能完成
@GetMapping(path = "/{id}", params = "method")
public ResponseEntity<Vehicle> dependOnMthod(@PathVariable Long id, String method) {
switch (method) {
case "select" :
return vehicleRepository.findById(id).map(vehicle -> new ResponseEntity<>(vehicle,HttpStatus.OK)).orElse(new ResponseEntity<>(null,HttpStatus.BAD_REQUEST));
case "delete" : {
vehicleRepository.deleteById(id);
return new ResponseEntity<>(null,HttpStatus.OK);
}
case "duplicate" :
return duplicateOne(id);
default:
return new ResponseEntity<>(null,HttpStatus.BAD_REQUEST);
}
}
private ResponseEntity<Vehicle> duplicateOne(Long id) {
Optional<Vehicle> findById = vehicleRepository.findById(id);
if(!findById.isPresent()) {
return new ResponseEntity<>(null,HttpStatus.BAD_REQUEST);
}
Vehicle oldOne = findById.get();
Vehicle newOne = new Vehicle();
newOne.setVeName(oldOne.getVeName());
newOne.setDescription(oldOne.getDescription());
return new ResponseEntity<>(vehicleRepository.save(newOne),HttpStatus.OK);
}
这个项目使用的jpa的ddl-auto就是create-drop,所以表结构还是JPA在操控,所以最开始可以选择在数据库中手动创建,也可以不手工创建,因为每次退出项目都会删除
这样可以利用PostMan来不断给后台发送请求,观察响应的结果。但是这里还是存在其他的问题,就是HTTP动词加上url必须是唯一的,不然会出现一些问题,这里的问题只能通过其他的处理逻辑进行解决
请求与响应
Web服务的数据交互很重要,这里先来看看HTTP协议的相关的内容
HTTP报文: 请求Request和响应Response属于Http报文的两种形式,客户端传递给服务端为请求,反之为响应,具有相同的结构
- 起始行: 描述请求或者响应的状态
- Header: HTTP头信息
- 空行: 有CRIF字符组成的空行,分割HTTP头和HTTP报文主体
- Body: 报文主体,用于搭载请求或者响应中的主体
@RequestParam 请求体中的参数注入
一般当请求体中的name和处理器方法的参数名一致的时候可以自动注入,但不一致的时候就需要这个注解进行协助,同时也可以使用其有用的属性
- name: Web的参数名,就是前台提交的name
- required: 是否必须传送,和上面的Mapping的params的类似
- defaultValue: 没有传输值的时候使用该默认值
当required为true时,如果没有传输这个参数,接口就会报错w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.UnsatisfiedServletRequestParameterException: Parameter conditions “method” not met for actual request parameters: ]
@PathVariable 获取请求url中的路径参数
RESTful风格的API接口,url包含所查询的元素,,之前的绑定都很简单,实际上会有多个路径参数,挥着将多个路径参数绑定给一个Map对象
- @GetMapping(value= “/{firstName}/{lastName}”, params = MULTI)
这里就是对应的多个路径参数,params指定的是路径变量的类型,多个路径变量,在方法中就使用多次@PathVarible即可
- @GetMapping(value= “/{firstName}/{lastName}”, params = IN_MAP)
IN_MAP就是将多个路径参数赋值给一个MAP对象,直接使用一个@PathVarible即可
除了上面的基本的基本的简单引用,多对多和多对map的params属性之外,还可以结合正则表达式进行参数过滤
@DeleteMapping("/{logName:[\\D]+}")
public User findOne(@PathVarible String loginName)
这里就是一个对于id的正则表达式,就是要匹配[]中的字符,这里\为转义,\D就是匹配所有的非0-9的数字,+代表匹配一个或者多个【综合来看就是匹配一个或者多个非数字的字符
@RequestHeader 读取请求头
之前的ReqeustParam读取的是请求体中的单个参数,而Request读取的是请求头中的内容,二者不同
//----请求行 GET /restDemo/api/vehicle/1?method=select HTTP/1.1
下面都是请求头,也都是键值对的形式;所以获取就可以借助ReqeustHeader获取
Host: localhost:8084
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:35.0) Gecko/20100101 Firefox/35.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Connection: keep-alive
常见的绑定方式:
- 读取单个头属性,就是指定键值,注入给一个变量
- 将所有的头属性获取之后绑定给一个Map
- 将所有头属性绑定到要给MultiValueMap实例
- 将所有头属性绑定给httpHeaders实例【推荐使用】
操作Map,需要使用到EntrySet,一个键值对就是一个记录Entry,通过entrySet方法就可以将Map变为一个entry记录集,之后再进行相关的map,filter,collect操作即可
@GetMapping("/greeting")
public String greeting(@RequestHeader("accept-language") String language) {
switch (language) {
case "zh":
return "你好";
case "en":
default:
return "Hello";
}
}
@GetMapping("/header-map")
public String headerMap(@RequestHeader Map<String,String> headersMap) {
return headersMap.entrySet().stream()
.map(stringStringEntry -> String.format("key=%s,value=%s",stringStringEntry.getKey(),stringStringEntry.getValue()))
.collect(Collectors.joining("\n","【","】"));
}
@GetMapping("/multi-value-map")
public String headerMultiMap(@RequestHeader MultiValueMap<String,String> headerMultiMap) {
return headerMultiMap.entrySet().stream()
.map(entry -> String.format("key=%s,value=%s",entry.getKey(),String.join("|",entry.getValue())))
.collect(Collectors.joining("/r/n"));
}
@GetMapping("/http-header")
public String useHttpHeaders(@RequestHeader HttpHeaders httpHeaders) {
return String.join(",",Optional.ofNullable(httpHeaders.get("Accept-Encoding")).orElse(new ArrayList<>()));
}
collect()中的Collectors.joining就是将流的个元素收集拼接,可以只是连接符,或者给出前缀和后缀; 使用Optional的ofNullable包裹之后,就可以使用orElse来进行空处理
@RequestBody和@ResponseBody
前面代表将请求体的内容序列化为对应的类实例【对象】,然后注入给相关的变量,后者为将对象反序列化为对应的JSON格式的字符串,再Restful服务中很常见
返回值默认是application/json,如果需要将其格式调整为application/xml,需要加入相关的依赖jar
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
如果交换的数据类型为xml,不是json,那么需要指定Header和produces的类型
@PostMapping(value = "/article", headers = "Accept=application/xml", produces = MediaType.APPLICATION-XML-VALUE)
@ResponseBody
public String saveArticleAndGetXML(@RequestBody SubmitArticleQuery queryArticle) {
BlogUser author = blogUserRepository.findUserByLoginName(queryArticle.getAuthorName());
if(author == null) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,"This author does not exist");
}
Article toSave = new Article();
toSave.setAuthor(author);
toSave.setTitle(queryArticle.getTitle());
toSave.setHeadline(queryArticle.getHeadline());
toSave.setContent(queryArticle.getContent());
toSave.setSlug(CommonUtil.toSlug(queryArticle.getTitle()));
repository.save(toSave);
return "Success";
}
Reponseentity处理HTTP响应
一个Web服务的返回值,大部分情况下关注的只是响应体的部分,而响应头和响应的状态码都是默认状态,如果需要对默认的状态码进行修改,就要使用这个ReponseEntity,REST风格也推荐使用ReponseEntity来封装处理的结果
@GetMapping("/response-test")
public ResponseEntity<String> getResponse(@RequestParam("veName") String userName) {
switch (userName) {
case "cfeng" : {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add("sessionID","12325788967868");
return new ResponseEntity<>("欢迎光临,Cfeng",httpHeaders,HttpStatus.OK);
}
default:
return new ResponseEntity<>("Sorry,you can't", HttpStatus.BAD_REQUEST);
}
}
可以查看响应结果,请求头确实添加了自定义的键值对
Connection: keep-alive
Content-Length: 20
Content-Type: text/html;charset=UTF-8
Date: Mon, 27 Jun 2022 12:17:21 GMT
Keep-Alive: timeout=60
sessionID: 12325788967868
GET http://localhost:8084/restDemo/api/vehicle/response-test?veName=cfen 400
参数验证validate
有一些注解不常用就会忘记,比如@Accessor,这个是Lombok的修饰注解,放在类上面就对所有的字段起作用,访问器,其chain属性为true就会再set方法返回当前set的对象
在构建程序的时候,需要进行参数验证,传统的方式就是将验证的逻辑写在业务逻辑中,Spring为了进行解耦合,就提供了一种Spring Validation的方式
Bean Validation 基础验证 引入相关的validation-starter,前台加上@Validated校验传入数据
Bean Validation是Spring Validation的基础部分,是JCP(Java Community Process)定义的标准化的JavaBean的验证API,提供了一组注解,标注对应的元素的验证方式,这里的验证可以看作和数据库中的约束的效果类似
- @Null 被标注的元素必须为空
- @NotNull 被标注的元素必须不为空
- @AssertTrue: 被标注的元素必须为True, AssertFalse
- Min(value) Max(value) 被标注的元素必须为数字,值的范围大于或者小于
- DecimalMin(value) DecimalMax(value) 被标注的元素必须为数字,其值必须大于等于或者小于等于
- Size(max,min) 在范围之内 类似与之前的BETWEEN AND
- Digis(integer,fraction) 数字的值在课接收的范围之内
- Past 被标注的元素必须是一个过去的日期
- Future: 被标注的元素必须是一个将来的日期
- Patten: 被标注的元素符合正则表达式…
Spring-validation包括Bean Validation的实现,也就是Hibernate Validation 【也就是建表的时候的约束】
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
这些注解标注在相关的属性上面,因为对于JPA全自动框架,是不需要手动创建表的,所以这里就是提供了JCP验证的规则来对字段进行约束
@NotNull(message = "the title must not be null")
private String title;
@NotNull(message = "the headLine must not be null")
private String headline;
@NotNull(message = "the content must not be null")
private String content;
@NotNull(message = "the author must not be null")
private String authorName;
这里操作之前的cfeng-blog中前台像后台提交的SubmitArticle,在对应的自动位置加上相关的约束之后,前台传入的参数,就可以使用==@Validated==校验
在对应的处理器方法位置,在@RequestBody后面加上这个注解,就会校验传入的对象的相关的属性,当然也可以放在前台校验
*/
@PostMapping("/article")
@ResponseBody
public String saveArticle(@RequestBody @Validated SubmitArticleQuery queryArticle) {
当前台给的authorName为空的时候,后台会给出异常:
MethodArgumentNotValidException: Validation failed for argument [0] in public java.lang.String indv.cfeng.controller.HtmlController.saveArticle(indv.cfeng.domain.SubmitArticleQuery): [Field error in object 'submitArticleQuery' on field 'authorName': rejected value [null]; codes [NotNull.submitArticleQuery.authorName,NotNull.authorName,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [submitArticleQuery.authorName,authorName]; arguments []; default message [authorName]]; default message [the author must not be null]] ]
这里就还是使用功能测试,直接测试麻烦
功能测试Controller,那么就需要使用mockMvc,来构建下面的桩模块,这里需要使用@ExtendWith,和@SpringBootTest,这样就可以创建一个容器mockMvc
@SpringBootTest
@ExtendWith(SpringExtension.class)
public class ValidationTests {
@Resource
private WebApplicationContext webApplicationContext;
private MockMvc mockMvc;
@BeforeEach
public void setup() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
}
@Test
public void whenSubmitWrongAuthor_thenReturn4xx() throws Exception {
SubmitArticleQuery submitArticleQuery = new SubmitArticleQuery();
submitArticleQuery.setTitle("title");
submitArticleQuery.setHeadline("hiehie");
submitArticleQuery.setContent("content is hehieh ");
mockMvc.perform(MockMvcRequestBuilders.post("/article").contentType(MediaType.APPLICATION_JSON)
.content(mapper.writeValueAsBytes(submitArticleQuery))).andExpect(MockMvcResultMatchers.status().is4xxClientError()).andDo(print())
}
}
自定义校验
首先要创建自定义注解,创建注解的方式: 首先就是要给出@Target,@Retention,@Constraint,@Documented, public @interface XXXX{} 这些就是创建一个注解最基本的,可以对比来创建
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(NotNull.List.class)
@Documented
@Constraint(
validatedBy = {}
)
public @interface NotNull {
String message() default "{javax.validation.constraints.NotNull.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface List {
NotNull[] value();
}
}
上面这是官方的NotNull的注解,发现其实NotNull中除了基本的NotNull之外,还有List注解,基本的结构就是先放桑格注解@Target,@Retention,@Documented,之后创建接口
创建自定义注解CfengAuthor
package indv.cfeng.validator;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = AuthorValidator.class)
@Documented
public @interface CfengAuthor {
String message() default "Author is not allowed";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
这里的@Constraint就是指定约束的验证的实现类
之后创建这个注解所需要的实现Validator
public class CfengAuthorValidator implements ConstraintValidator<CfengAuthor,String> {
private final List<String> VALID_AUTHORS = Arrays.asList("xiaoHuan","Cfeng");
@Override
public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
return VALID_AUTHORS.contains(s);
}
}
也就是将相关的业务逻辑放在validator注解的实现类中做
@NotNull(message = "the author must not be null")
@CfengAuthor
private String authorName;
所以自定义的校验主要是创建注解,并且在实现ConstraintValidator接口,并且实现其中的isValid方法,该方法的返回值就是校验是否通过的boolean值
错误处理
程序出现错误会抛出非正常信息Throwable,Throwable分为错误Error和异常Exception,异常和人为的bug不同,异常在程序中代表的是当前程序无法处理的情况,比如一个值为空,用户给出的URL没有找到对应的资源
在Java开发中,检查型异常通常要进行try/catch异常处理,在Spring中之前提过全局的异常处理,在SpringBoot中还有其他的方式
使用HandlerExceptionResolver处理异常
@ExceptionHandler虽然可以满足很大一部分要求,但是不进行特殊处理的情况下只能处理单个Controller的异常,面对多个Controller抛出的异常,需要借助HandlerExceptionResolver,可以解决程序内部任何的异常,可以实现RestFul服务的统一异常处理
HandlerExceptionResolver是一个公共接口,使用的方式是自定义一个处理类;这个接口已经有一些默认的实现类
- ExcepitonHandlerExceptionResolver: 这个处理类就是让@ExceptionHandler生效的组件
- DefaultHandlerExceptionResolver:用于将标注的Spring异常解析为对应的Http状态码
- ResponseStatusExceptionResolver: 与注解@ResponseStatus一起使用,将自定义的异常与相关的状态码进行对应
@ResponseStatus(value = HttpStatus.BOT_FOUND)
public class MyException extends Exception{
public MyException() {
}
public MyException(String message) {
super(message);
}
}
自定义处理类的母的就是控制响应体的内容,REST服务的响应需要JSON格式的响应内容[XML],所以可以创建一个处理器类处理异常
@Component
@Slf4j
public class RestResponseStatusExceptionResolver extends AbstractHandlerExceptionResolver {
@Override
protected ModelAndView doResolveException(HttpServletRequest request,HttpServeltResponse response,Object Handler,Exception ex) {
try{
if(ex instanceof IllegalArgumentException) {
return handleIllegalArgument((IllegalArgumentException) ex,response,request);
}
}catch(Excetption handlerException) {
log.warn("Handling of[" + ex.getClass.getName + "]resulted in Exception" , handlerException);
}
return null;
}
private ModelAndView handleIllegalArgument(IllegalArgumentException ex, HttpServletResponse, HttpServletRequest request) thorows IOException {
response.sendError(HttpServletResponse.SC_CONFLICT);
String accept = request.getHeader(HttpHeaders.ACCEPT);
return new ModelAndView();
}
}
可以看到这样处理异常还是稍微有些复杂的,最主要就是创建一个异常的处理对象,这个类是继承的AbstractHandlerExceptionResolver,之后重写doResolveException方法来处理异常即可
引入@ControllerAdivice就可以将该类下面的@ExceptionHandler方法在全局对异常进行处理,所以直接使用该注解就很方便,这是Spring中就提到过的
Spring的全局异常处理 类上面加@ControllerAdvice,方法上加@ExceptionHandler
这里的@ControllerAdivce就是相当于会创建一个异常处理对象放入容器中
首先就是之前Spring位置就提到过的全局异常处理,就是利用AOP将异常处理逻辑剥离,主要就是处理Controller层的注解【详见之前的blog】
异常处理方法的返回值类型可以是ModelAndView、Model、Map,还可以是void,或者HttpEntity和ResponseEntity包装的结果; 而签名包括异常的类型,或者请求响应对象和相关的流,以及Model
* Spring提供的全局异常处理
*/
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(value = {Exception.class})
public ResponseEntity<String> doException() {
log.info("这里必须要在注解中指定异常的类型,这里就所有的异常都是这个方法进行处理");
return new ResponseEntity<>("发生了异常,处理了", HttpStatus.OK);
}
}
这里就是发生异常之后就会跳转到这个处理器方法执行,返回的结果给到响应
这里可以测试,是手动测试
@GetMapping("/writing")
public String writeArticle(Model model) throws Exception {
String op = null;
if(op.equals("zhangsan")) return "你好";
这里会抛出空指针异常,抛出异常后会直接跳转到全局的异常处理,因为这里是直接将异常抛出,会被handler捕获
2022-06-27 22:55:01,408 INFO [http-nio-8086-exec-1] indv.cfeng.Interceptor.LogRequestInterceptor: [preHandle] [org.apache.catalina.connector.RequestFacade@f3a892d] [GET]/writing?
2022-06-27 22:55:01,417 INFO [http-nio-8086-exec-1] indv.cfeng.handler.GlobalExceptionHandler: 这里必须要在注解中指定异常的类型,这里就所有的异常都是这个方法进行处理
2022-06-27 22:55:01,438 WARN [http-nio-8086-exec-1] org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver: Resolved [java.lang.NullPointerException: Cannot invoke "String.equals(Object)" because "op" is null]
2022-06-27 22:55:01.438 [http-nio-8086-exec-1] WARN (o.s.w.s.m.m.a.ExceptionHandlerExceptionResolver2022-06-27 22:55:01,439 INFO [http-nio-8086-exec-1] indv.cfeng.Interceptor.LogRequestInterceptor: [afterCompletion][org.apache.catalina.connector.RequestFacade@f3a892d][exception:null]
可以看到控制台的日志清晰记录了本次的异常和相关的输出的信息
抛出ResourceStuatusException异常
上面的全局异常处理可以解决一个切面的问题,但是如果只是针对少量的接口进行异常处理控制其返回HTTP状态码和错误的原因,可以直接使用ResponseStatusException
@GetMapping(value = "/{id}")
public Foo findById(@PathVarible("id") Long id,HttpServletResponse response) {
try{
Foo resourceById = RestPrconditions.checkFound(service.findOne(id));
eventPublisher.publishEvent(new SingleResourceRetrievedEvent(this,response));
return resourceById;
}catch(MyResourceNotFoundException exc) {
throw new ResponseStatusExcepiton(HttpStatus.NOT_FOUND,"Foo Not Found",exc);
}
}
也就是发生异常的时候直接抛出一个ResponseStatusExcepiton异常,这个异常就会给出Http状态码,同时给出提示的信息和相关的异常
Swagger文档
在前后端分离的情况下,前后端的开发人员不同,这个时候,就需要维护一份文档,接口文档在项目初期帮助开发人员快速理解,也方便后期的维护,SpringBoot中有一款自动生产API文档的工具Swagger
Swagger主要用作RESTful API的描述和调试,集成了HTML、JavaScript和CSS前端资源,从符合Swagger规范的API动态生成可以交互的接口文档,后来重命名为OpenAPI规范
要使用Swagger,需要配置Springfox维护的springfox-boot-starter依赖项来使用Swagger
<!--使用Swagger/OpenAPI需要springfox-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
接下来就可以自动生成相关的接口文档了,接口文档的生成依赖的是Docket
生成接口文档,配置Docket Bean
要配置一个Docket,需要创建要给配置类,这个配置类需要加上@EnableOpenApi,表明这个类是配置Docket的
import io.swagger.annotations.ApiOperation;
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.oas.annotations.EnableOpenApi;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
@EnableOpenApi
@Configuration
public class SwaggerConfig {
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.OAS_30)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("Swagger接口文档")
.description("整合实例")
.version("1.0")
.build();
}
}
创建好这个配置类之后,相当于注入了Docket对象后,就可以启用接口文档了,可以分贝访问http://localhost:8080/v2/api-docs和 /v3/api-docs访问Swagger2规范的接口文档和OpenAPI3的接口文档
同时也可以在http://localhost:8080/swagger-ui/index.html访问接口文档页面
使用注解生成文档的内容
上面配置Docket之后就可以扫描项目中的注解生成对应的Swagger文档了,之后就是在项目开发的过程中,使用相关的注解来生成接口
- @Api: 放在Controller上面,将该类标记为Swagger类型资源: tags指定该类的作用,参数的类型为String数组
- @ApiOpreation: 放在接口的方法上面,表述特定的路径的操作,value为方法的用途和作用,notes为注意事项
- @ApiModel: 放在试题类上面,描述实体作用,desctiption描述实体的作用
- @ApiModelPropertity: 放在实体的属性上面,value为描述,name为属性名称,required为是否必选
- @ApiImplicitParam: 用在普通的方法上面,描述隐含的参数 name表达参数名,value为说明,dataType为数据类型,paramType: 描述参数的类型,位置,比如path,body,header等
- @ApiImplicitParams: 方法上面,包含多个@ApiImplicitParam
- @ApiParam: 方法、参数,描述请求的要求和说明 name为参数名,vlaue为描述,default为参数默认值,required为是否必选
- @ApiResponse: 请求的方法上面,描述不同的响应,code为状态码,message为响应的信息
- @ApiResponses: 多个…
比如这里演示一下Controller
@Api(tags = "博客文章管理模块")
@RestController
@RequestMapping("/api/article")
public class RestArticleController {
private final ArticleRepository repository;
public RestArticleController(ArticleRepository repository) {
this.repository = repository;
}
@ApiOperation(value = "无参的Get请求", notes = "注意这里的model的作用就是视图转发的时候装土相关的数据")
@GetMapping("/")
public Iterable<Article> findAll(Model model) {
@ApiOpration(value = "下一个生日",notes = "输入出生的年月日,计算到下一个生日的天数")
@ApiResponses({
@ApiResponse(code = 400, message = "输入日期大于当前日期")
@ApiResponse(code = 200, message = "成功")
})
然后具体的属性使用@ApiModelProperty进行说明
在接口的方法加上相关的注解就会自动将其放入到接口文档中,这些工作都是Docket完成😫
如果方法还要传入参数,可以使用@ApiImplictParam进行说明,同时在参数列表中使用@ApiParam说明相关需要说明的参数的位置等信息??
|