【springBoot开发技术】拦截过滤器,Restful服务详细介绍

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";
    }

    /**
     * controller中的方法定义为私有方法,只是在相关的处理器方法进行调用
     */
    private boolean authenticate(SessionQuery sessionQuery) {
        //认证,简单判断是否为admin和123456
        return Objects.equals(sessionQuery.getUsername(),"admin") && Objects.equals(sessionQuery.getPassword(),"123456");
    }

    /**
     * 证明,将登录的凭证返回给用户
     */
    private void certificate(HttpServletResponse response) {
        //将登录凭证以Cookie的形式返回给客户端
        Cookie credential = new Cookie("sessionId","test-token");
        //将令牌返回给用户
        response.addCookie(credential);
    }

之后编写过滤器进行过滤,过滤器的WebFilter注解就可以设置需要过滤的所有的url,这里的过滤就判断是否有Session即可

@Slf4j
@WebFilter(urlPatterns = "/secret/*") //通过该注解可以指定webFilter的过滤的路径,和之前的Servlet时代类似
public class SessionFilter implements Filter {

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        //读取Cookie,这里使用Optional进行包裹处理空指针,这里需要将request转为HttpServletRequest,同时还是要使用orElse指定空的时候的赋值
        Cookie[] cookies = Optional.ofNullable(((HttpServletRequest)servletRequest).getCookies()).orElse(new Cookie[0]);
        //判断是否有相关的session
        boolean unauthorized = true;//是否验证通过
        //之前模拟的登录是创建了一个Cookie为sessionId和test-token
        for(Cookie cookie : cookies) {
            if("sessionId".equals(cookie.getName()) && "test-token".equals(cookie.getValue())) {
                //验证成功继续执行,否则抛出401异常
                unauthorized = false;
            }
        }

        if(unauthorized) {
            //同时输出日志
            log.error("UNAUTHORIZED");//log就是简化后的logger日志记录器
           //响应401
            unauthorizedResp(servletResponse);
        }else {
            //放行
            filterChain.doFilter(servletRequest,servletResponse);
        }
    }

    /**
     * 向响应中写回401错误
     */
    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;
    }

    //处理程序执行后调用,这里也就简单test
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("[postHandle]");
    }

    //请求响应并且视图生成完毕,String.format格式化使用较多,语法格式和之前的C类似
    @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));
    }

    /**
     * 从请求中获取参数,私有方法;获取参数拼接为?name=value&name=value.....
     */
    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) {
        //将容器中的拦截器对象注册,addPathPatterns就是设置拦截的路径
        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) {
        //event就是事件源
        log.info("Cfeng, the applicationPreparedEvent");
    }
}

监听器有了,要能够使用监听器,需要主类发现,【拦截器注册之后主类会自动去查询WebMvcConfigure类】— 所以主类可以查询的,其他的诸如过滤器,监听器,配置类都需要发现 ,还有Mybatis的mapper也是需要发现的,但是JPA只要在主类包下面,就可以自动扫描Entity和Repository,不在的也需要相关的scan

@SpringBootApplication //主类SpringBoot关键注解,作用就是自动配置,ComponentScan和配置类
@EnableConfigurationProperties({BlogProperties.class, FileStorageProperties.class}) //扫描需要进行配置文件属性注入的类
@ServletComponentScan //扫描filter即用@WebFilter修饰的类
public class BlogApplication {

    public static void main(String[] args) {
        //接收这个容器为后面的test做准备,不需要,直接功能测试即可
        //run方法除了可以使用类方法,也可以创建一个实例再使用一个args的实例方法,为了注册监听器,使用实例
        SpringApplication application = new SpringApplication(BlogApplication.class);
        //手动创建一个监听器实例注册,拦截器位置因为容器已经创建,而这里容器还没有创建,不能DI
        //拦截器位置也可以手动new,但是DI方便,只要加上@component就可以实现了
        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就可以作为事件服务

  • 创建事件源,也就是服务的发布者publisher
@Slf4j
@RequiredArgsConstructor
@Component
public class MessageEventPublisher {

    private final ApplicationEventPublisher applicationEventPublisher;

    /**
     * 借助ApplicaitonEventPublisher对象进行事件的发布
     * @param message
     */
    public void publishEvent(String message) {
        log.info("publish an event.Message:" + message);
        //发布事件服务,需要发布这个事件,source就是这个事件源
        applicationEventPublisher.publishEvent(new MessageEvent(this,message));
    }
}
  • 创建事件监听器,创建事件监听器有两种范式,一种就是上面的内置事件监听的方式,使用ApplicationListener
@Slf4j
@Component //因为注册的时候要一个对象,这为了测试,所以直接单例Bean
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
    @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) {
        //依靠BeanWrapper这个bean包裹器来获取到bean的相关信息
        final BeanWrapper wrapperSource = new BeanWrapperImpl(source);
        //通过Stream流来获取信息
        return Stream.of(wrapperSource.getPropertyDescriptors())
                .map(FeatureDescriptor::getName)
                .filter(propertyName -> Objects.isNull(wrapperSource.getPropertyValue(propertyName)))
                .collect(Collectors.toList());
    }
  1. Optional类的使用: 为了避免查询结果出现空的情况【程序报错空指针】所以需要进行判空,不要使用 == null; 而是直接使用java8提供的方式,先封装为Optional<类>, 之后使用.isPresent方法判断是否存在,如果不存在,就直接…,否则就是用封装对象的get方法获得包装之前的对象
  2. ReponseEntity的使用: Rest风格的Controller都是返回的响应体,对应的就是ReponseEntity,所以每一个返回结果都可以是ReponseEntity,其包装的就是之前想要放回的数据,好处就是其return的时候需要指定HttpStatus,也就是状态码,符合前后端交互的需求
  3. 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实现上面的接口,上面的就是前后端交互的接口了

//首先简单使用CLI创建项目,配置port和h2的数据源
//创建表
@Entity
@Table(name = "t_vehicle")//可以通过@Teable指定对应的创建在数据库中的表名,如果不指定就会创建同名的表
@Accessors(chain = true)
@Getter
@Setter
@ToString
public class Vehicle {
    //主键
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) //默认是auto的
    private Long id;

    private String name;

    //描述
    private String description;

}

这里测试就创建一个单表即可

接下来就是创建repository来操作数据库

public interface VehicleRepository extends JpaRepository<Vehicle,Long> {
    //这里实现了基本的CRUD,不忙添加新的操作
}

之后的Service因为太简单不创建了,直接用Controller调用Repository即可

@RestController
@RequestMapping("/api/vehicle") //rest风格的接口
@RequiredArgsConstructor
public class VehicleController {

    private final VehicleRepository vehicleRepository;

    //依次实现之前的所有的接口
    /**
     * 查询所有的Vehicle数据
     * REST风格返回的应该是ReposeEntity,其中包括返回的数据,就最初开发的时候直接返回的json等,和响应的http状态
     */
    @GetMapping("/list")
    public ResponseEntity<List<Vehicle>> getVehicleList() {
        return new ResponseEntity<>(vehicleRepository.findAll(), HttpStatus.OK);
    }

    /**
     * 根据ID查询一条记录
     * 对查询的结果需要使用Optional的map进行包装,当未空的时候使用orElse指定返回值
     * map就是对包裹后的数据操作的功能函数,可以使用Lambda表达式
     */
    @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));
    }

    /**
     * 上传一条Vehicle记录
     * 返回值就是上传的数据; 这里就可以是repository的save方法的返回值
     */
    @PostMapping("/")
    public ResponseEntity<Vehicle> addVehicle(@RequestBody Vehicle vehicle) {
        return new ResponseEntity<>(vehicleRepository.save(vehicle),HttpStatus.OK);
    }

    /**
     * 替换一条Vehicle记录,更新修改; 这里会将上传的数据自动更新到数据库
     * 返回值也是修改后的Vehicle即可
     */
    @PutMapping("/")
    public ResponseEntity<Vehicle> replaceVehicle(@RequestBody Vehicle vehicle) {
        //首先获取上传数据的id,找到相关的记录
        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);
    }

    /**
     * 修改记录,服务端修改其某个字段
     * patch是修改的具体的属性
     */
    @PatchMapping("/")
    public ResponseEntity<Vehicle> modifyVehicle(@RequestBody Vehicle vehicle) {
        //修改Vehicle记录
        Optional<Vehicle> findById = vehicleRepository.findById(vehicle.getId());
        Vehicle oldOne;
        if(!findById.isPresent()) {
            return new ResponseEntity<>(null,HttpStatus.BAD_REQUEST);
        } else {
            //先使用Optional包裹,如果存在再使用get获得方法
            oldOne = findById.get();
        }
        //Patch方法就是修改操作,和Put有区别,put是直接整个替换的操作
        //这里将所有的非空属性赋值给newOne
        Vehicle newOne = new Vehicle();
        List<String> nullProperties = this.getNullProperties(oldOne);
        BeanUtils.copyProperties(newOne,oldOne,nullProperties.toArray(new String[0]));
        //crud的save返回值就是保存对象
        return new ResponseEntity<>(vehicleRepository.save(newOne),HttpStatus.OK)
    }

    /**
     * 删除一条记录,根据id
     */
    @DeleteMapping("/{id}")
    public ResponseEntity<Vehicle> deleteVehicle(@PathVariable Long id) {
        vehicleRepository.deleteById(id);
        return new ResponseEntity<>(null,HttpStatus.OK);
    }

    /**
     * 私有方法: 获取空属性的所欲的属性名
     * 通过wrapper将bean将对象的相关信息给过滤
     */
    private List<String> getNullProperties(Object source) {
        //依靠BeanWrapper这个bean包裹器来获取到bean的相关信息
        final BeanWrapper wrapperSource = new BeanWrapperImpl(source);
        //通过Stream流来获取信息
        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 = {};
        //响应的JSON数组
        let formArray = $("form").serializeArray(); //序列化表单元素(类似 .serialize () 方法 ),返回 JSON 数据结构数据。. 注意: 此方法返回的是 JSON 对象而非 JSON 字符串
        $.each(formArray,function (i,item) {
            formObj[item.name] = item.value;
        });
        //使用AJAX,创建POST请求
        $.ajax({
            type: 'POST',
            url: "/restDemo/api/vehicle/",
            data: JSON.stringify(formObj), //将一个 JavaScript 对象或值转换为 JSON 字符串;JSON字符串,不是JSON对象
            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
	###配置数据库连接池相关参数database connect pool
	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的相关,包括打印sql,数据库类型,还有就是hibernate的相关的策略
	jpa:
		database: mysql
		show-sql: true
		hibernate:
			ddl-auto: update #update自动更新表结构,create-drop是进入创建,退出删除,create-是每次加载的时候重新创建,会丢失数据
			naming: #命名策略,也就是创建表的时候的映射的策略,使用org.hibernate.cfg.ImprovedNamingStrategy是无修改命名,而下面的是遇到大写转为_
				strategy: org.springframework.boot.orm.jpa.hibernate.SpringPhyicalNamingStategy
			#properties:这里的配置可以不用配置,上面选定了Driver
				#hibernate:
					#dialect: org.hibernate.dialect.MySQL5Dialect

这样就可以成功连接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动词不能完成

 /**
     * 这个方法通过扩展一个method参数,通过传入不同的参数,就可以实现不同的操作,这里就相当于是之前的Get和Delete请求结合,并且会结合复制的操作
     * 将上面的GetMapping给注释掉
     * @param id
     * @param method
     * @return
     */
    @GetMapping(path = "/{id}", params = "method") //请求中必须包含参数method,通过这个mehtod来指定不同的操作,会自动注入给同名的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: 报文主体,用于搭载请求或者响应中的主体

HTTP报文结构 的图像结果

@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

常见的绑定方式:

  1. 读取单个头属性,就是指定键值,注入给一个变量
  2. 将所有的头属性获取之后绑定给一个Map
  3. 将所有头属性绑定到要给MultiValueMap实例
  4. 将所有头属性绑定给httpHeaders实例【推荐使用】

操作Map,需要使用到EntrySet,一个键值对就是一个记录Entry,通过entrySet方法就可以将Map变为一个entry记录集,之后再进行相关的map,filter,collect操作即可

    /**
     * 测试请求头Header,请求头和请求体都是键值对的形式,所以都是可以进行键值的获取,获取请求头使用的是@requestHeader
     * 常见的获取方式:
     * 1. 读取单个头属性,就是指定键值,注入给一个变量
     * 2.将所有的头属性获取之后绑定给一个Map
     * 3.将所有头属性绑定到要给MultiValueMap实例
     * 4.将所有头属性绑定给httpHeaders实例【推荐使用】
     */
    @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) {
        //返回一个头属性拼接的字符串,所以需要使用java8的流
        return headersMap.entrySet().stream()
                .map(stringStringEntry -> String.format("key=%s,value=%s",stringStringEntry.getKey(),stringStringEntry.getValue()))
                .collect(Collectors.joining("\n","【","】"));
    }

    //绑定给MultiValueMap
    @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"));
    }

    //绑定给HttpHeaders
    @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
    //@RequestBody就是将返回的Json对象整个注入一个某一个对象,而不是一个一个值注入
    public String saveArticleAndGetXML(@RequestBody SubmitArticleQuery queryArticle) {
        //接收POST请求,前台传入的是author的姓名
        BlogUser author = blogUserRepository.findUserByLoginName(queryArticle.getAuthorName());
        if(author == null) {
            //说明没有这个author,应该报错400
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST,"This author does not exist");
        }
        //Article和submitArticle主要区别就是author不同,slug
        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来封装处理的结果

    /**
     * REST风格中推荐使用ResponseEntity来封装响应结果,因为除了响应体之外,还可以操作响应的状态码响应头
     */
    //添加自定义头,并且返回不同的状态码
    @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
    //@RequestBody就是将返回的Json对象整个注入一个某一个对象,而不是一个一个值注入
    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) //和@RunWith类似,就是将容器的对象DI
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 ");
        //author为空来验证结果
        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.*;

/**
- @author Cfeng
- @date 2022/6/27
  */
  @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) {
        //判断相关的业务逻辑,这里的s代表的就是放置的字段的值,上面的String就是这个字段的数据类型
        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
        }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;

/**
 * @author Cfeng
 * @date 2022/6/28
 * 该类的作用主要就是创建Swagger对象,需要加上@EnableOpenapi注解
 */

@EnableOpenApi
@Configuration
public class SwaggerConfig {
    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.OAS_30)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class)) //使用@ApiOpration的Contoller就会被添加到接口文档中【就是之前的处理器】
                .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 {
    //使用的final + 构造器的方式自动注入,可以使用Lombok简化
    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 = "成功")
        })
        
        
       //在实体类中常用的就是@ApiModel
        然后具体的属性使用@ApiModelProperty进行说明

在接口的方法加上相关的注解就会自动将其放入到接口文档中,这些工作都是Docket完成😫

如果方法还要传入参数,可以使用@ApiImplictParam进行说明,同时在参数列表中使用@ApiParam说明相关需要说明的参数的位置等信息☮️

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Boot中可以使用JUnit或者Spring提供的测试框架测试RESTful服务。以下是一些测试RESTful服务的方法: 1. 使用Spring提供的MockMvc进行测试 MockMvc是Spring提供的一个测试框架,可以模拟HTTP请求和响应,测试Controller的处理逻辑。使用MockMvc时,需要使用MockMvcBuilders构建MockMvc对象,然后使用MockMvc对象发起HTTP请求。 例如,以下代码使用MockMvc测试一个简单的GET请求: ```java @RunWith(SpringRunner.class) @SpringBootTest @AutoConfigureMockMvc public class MyControllerTest { @Autowired private MockMvc mockMvc; @Test public void testGet() throws Exception { mockMvc.perform(MockMvcRequestBuilders.get("/path")) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.content().string("Hello World")); } } ``` 2. 使用JUnit测试RestTemplate RestTemplate是Spring提供的一个RESTful服务客户端,可以发起HTTP请求。在测试RESTful服务时,可以使用RestTemplate发起HTTP请求,然后判断响应是否符合预期。 例如,以下代码使用RestTemplate测试一个简单的GET请求: ```java @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class MyControllerTest { @Autowired private TestRestTemplate restTemplate; @Test public void testGet() throws Exception { ResponseEntity<String> response = restTemplate.getForEntity("/path", String.class); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(response.getBody()).isEqualTo("Hello World"); } } ``` 以上是两种常用的测试RESTful服务的方法,根据实际情况选择适合的测试方法即可。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值