• 【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";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    之后可以再此基础上创建一个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);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    之后编写过滤器进行过滤,过滤器的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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    @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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    在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("/**");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    过滤器的过滤的路径直接在注解位置进行声明,而拦截器的拦截的路径则是在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");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    监听器有了,要能够使用监听器,需要主类发现,【拦截器注册之后主类会自动去查询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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    自定义事件

    自定义事件进行代码解耦,要使用自定义事件,需要事件,事件源和事件监听器

    • 创建自定义事件类Event,继承ApplicationEvent类,
    @Getter
    public class MessageEvent extends ApplicationEvent {
        
        //
        private final String message;
        
        public MessageEvent(Object source, String message) {
            super(source);
            this.message = message;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    这里就实现了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));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 创建事件监听器,创建事件监听器有两种范式,一种就是上面的内置事件监听的方式,使用ApplicationListener
    @Slf4j
    @Component //因为注册的时候要一个对象,这为了测试,所以直接单例Bean
    public class MessageEventListener implements ApplicationListener<MessageEvent> {
        @Override
        public void onApplicationEvent(MessageEvent event) {
            //事件激活的时候,触发
            log.info("Other business...Message" + event.getMessage());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    除了实现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  自定义事件");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    可以查看结果

    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  自定义事件
    
    • 1
    • 2

    异步事件

    在默认情况下,事件的发布与监听是同步执行的。当要用到异步事件时,需要进行额外的支付。具体方式在创建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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    这里相当于是创建一个异步的Bean,异步事件的主要依靠就是ApplicationEventMulticaster接口,创建的就是下面的一个简单实现类Simple~

    Controller开发策略 — Optional,ResponseEntity

    其实对于java8中的包裹类的使用,JPA本身就是在运用,比如CrudRepository就是

    Optional<T> findById(ID id);
    
    • 1

    所以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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    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">
    
    • 1
    • 2

    自动配置之后,就不需要再写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;
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

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

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

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

    之后的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());
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99

    从这里开始不使用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}}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    这里最主要的就是将前台的表单提交的数据进行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 (?, ?, ?)
    
    • 1
    • 2
    • 3
    • 4

    这里就可以在数据库中查询到这个数据

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    这样就可以成功连接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 {};
        ........
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    可以看到就是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);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    这个项目使用的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即可
    
    • 1
    • @GetMapping(value= “/{firstName}/{lastName}”, params = IN_MAP)
    IN_MAP就是将多个路径参数赋值给一个MAP对象,直接使用一个@PathVarible即可
    
    • 1

    除了上面的基本的基本的简单引用,多对多和多对map的params属性之外,还可以结合正则表达式进行参数过滤

    @DeleteMapping("/{logName:[\\D]+}")
    public User findOne(@PathVarible String loginName)
    
    • 1
    • 2

    这里就是一个对于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
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    常见的绑定方式:

    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<>()));
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    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>
    
    • 1
    • 2
    • 3
    • 4

    如果交换的数据类型为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";
        }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    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);
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    可以查看响应结果,请求头确实添加了自定义的键值对

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    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>
    
    • 1
    • 2
    • 3
    • 4

    这些注解标注在相关的属性上面,因为对于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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    这里操作之前的cfeng-blog中前台像后台提交的SubmitArticle,在对应的自动位置加上相关的约束之后,前台传入的参数,就可以使用==@Validated==校验

    在对应的处理器方法位置,在@RequestBody后面加上这个注解,就会校验传入的对象的相关的属性,当然也可以放在前台校验

         */
        @PostMapping("/article")
        @ResponseBody
        //@RequestBody就是将返回的Json对象整个注入一个某一个对象,而不是一个一个值注入
        public String saveArticle(@RequestBody @Validated SubmitArticleQuery queryArticle) {
    
    • 1
    • 2
    • 3
    • 4
    • 5

    当前台给的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]] ]
    
    • 1

    这里就还是使用功能测试,直接测试麻烦

    功能测试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())
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    自定义校验

    首先要创建自定义注解,创建注解的方式: 首先就是要给出@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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    上面这是官方的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 {};
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    这里的@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);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    也就是将相关的业务逻辑放在validator注解的实现类中做

    @NotNull(message = "the author must not be null")
        @CfengAuthor
        private String authorName;
    
    • 1
    • 2
    • 3

    所以自定义的校验主要是创建注解,并且在实现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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    自定义处理类的母的就是控制响应体的内容,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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    可以看到这样处理异常还是稍微有些复杂的,最主要就是创建一个异常的处理对象,这个类是继承的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);
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    这里就是发生异常之后就会跳转到这个处理器方法执行,返回的结果给到响应

    这里可以测试,是手动测试

        @GetMapping("/writing")
        public String writeArticle(Model model) throws Exception {
            String op = null;
            if(op.equals("zhangsan")) return "你好";
    
    • 1
    • 2
    • 3
    • 4

    这里会抛出空指针异常,抛出异常后会直接跳转到全局的异常处理,因为这里是直接将异常抛出,会被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]
    
    • 1
    • 2
    • 3
    • 4

    可以看到控制台的日志清晰记录了本次的异常和相关的输出的信息

    抛出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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    也就是发生异常的时候直接抛出一个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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    接下来就可以自动生成相关的接口文档了,接口文档的生成依赖的是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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    创建好这个配置类之后,相当于注入了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进行说明
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

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

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

  • 相关阅读:
    LeetCode 318 周赛
    Nginx 反向代理 SSL 证书绑定域名
    云端IDE的技术选型1
    linux笔记(7):东山哪吒D1H使用framebuffer控制HDMI直线
    nginx 安全配置
    设计模式中Monoid/Foldables
    java中的多态
    docker命令总结
    JuiceFS 在多云存储架构中的应用 | 深势科技分享
    SkyWalking分布式链路追踪学习
  • 原文地址:https://blog.csdn.net/a23452/article/details/125494900