springboot2 (3) 高级操作

486 阅读9分钟

1. Servlet原生组件

概念: springboot支持原生的servlet组件,如servlet过滤器和servlet监听器等:

  • 开发原生servlet类 c.y.s.servlet.BeanServlet,无需添加注解。
  • 开发原生过滤器类 c.y.s.servlet.filter.BeanFilter,无需添加注解。
  • 开发原生监听器类 c.y.s.servlet.listener.BeanListener,无需添加注解。
  • 开发配置类 c.y.s.servlet.BeanServletConfig
    • IOC o.s.b.w.s.ServletRegistrationBean 类,利用构造传入servlet实例和路由。
  • 开发配置类 c.y.s.servlet.filter.BeanFilterConfig
    • IOC o.s.b.w.s.FilterRegistrationBean 过滤器链类。
    • filters.setFilter():在过滤器链中加入自定义过滤器。
    • filters.addUrlPatterns():在过滤器链中加入自定义过滤器拦截规则。
  • 开发配置类 c.y.s.servlet.listener.BeanListenerConfig
    • IOC o.s.b.w.s.ServletListenerRegistrationBean 监听器类,利用构造传入监听器实例。
    • 每个监听对应一个 @Bean
  • psm测试:/api/servlet/bean

概念: /springboot/

  • res:c.y.s.servlet.BeanServlet
package com.yap.springboot2.servlet;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author yap
 */
public class BeanServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().write("This is BeanServlet!");
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}
  • res:c.y.s.servlet.BeanServletConfig
package com.yap.springboot2.servlet;

import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author yap
 */
@Configuration
public class BeanServletConfig {

    @SuppressWarnings("all")
    @Bean
    public ServletRegistrationBean servletRegistrationBean() {
        return new ServletRegistrationBean(new BeanServlet(), "/api/servlet/bean");
    }
}

  • res:c.y.s.servlet.filter.BeanFilter
package com.yap.springboot2.servlet.filter;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author yap
 */
public class BeanFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) {
        System.out.println("BeanFilter init()...");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;
        System.out.println("BeanFilter doFilter(): " + req.getRequestURI());
        chain.doFilter(req, resp);
    }

    @Override
    public void destroy() {
        System.out.println("BeanFilter destroy()...");
    }

}
  • res:c.y.s.servlet.filter.BeanFilterConfig
package com.yap.springboot2.servlet.filter;

import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author yap
 */
@Configuration
public class BeanFilterConfig {

    @SuppressWarnings("all")
    @Bean
    public FilterRegistrationBean filterRegistrationBean() {
        FilterRegistrationBean filters = new FilterRegistrationBean();
        filters.setFilter(new BeanFilter());
        // 不支持部分模糊,如 `/test` 或 `/*test`
        filters.addUrlPatterns("/api/servlet/*");
        return filters;
    }
}

  • res:c.y.s.servlet.listener.BeanListener
package com.yap.springboot2.servlet.listener;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

/**
 * @author yap
 */
public class BeanListener implements ServletContextListener {

    @Override
    public void contextInitialized(ServletContextEvent sce) {
        System.out.println("BeanListener contextInitialized()...");
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        System.out.println("BeanListener contextDestroyed()...");
    }
}
  • res:c.y.s.servlet.listener.BeanListenerConfig
package com.yap.springboot2.servlet.listener;

import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author yap
 */
@Configuration
public class BeanListenerConfig {

    @Bean
    public ServletListenerRegistrationBean servletListenerRegistrationBean() {
        return new ServletListenerRegistrationBean<>(new BeanListener());
    }
}

1.1 注解配置方案

流程:

  • 开发原生servlet类 c.y.s.servlet.ScanServlet,标记 @WebServlet
  • 开发原生过滤器类 c.y.s.servlet.filter.ScanFilter,标记 @WebFilter
  • 开发原生监听器类 c.y.s.servlet.listener.ScanListener,标记 @WebListener
  • 在启动类中使用 @ServletComponentScan 扫描servlet类,过滤器类和监听器类所在包。
    • @ServletComponentScan("com.yap.springboot2.servlet")
  • psm测试:/api/servlet/scan

概念: /springboot/

  • res:c.y.s.servlet.ScanServlet
package com.yap.springboot2.servlet;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author yap
 */
@WebServlet("/api/servlet/scan")
public class ScanServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().write("This is ScanServlet!");
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}
  • res:c.y.s.servlet.filter.ScanFilter
package com.yap.springboot2.servlet.filter;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author yap
 */
@WebFilter("/api/servlet/*")
public class ScanFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) {
        System.out.println("ScanFilter init()...");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;
        System.out.println("ScanFilter doFilter(): " + req.getRequestURI());
        chain.doFilter(req, resp);
    }

    @Override
    public void destroy() {
        System.out.println("ScanFilter destroy()...");
    }

}
  • res:c.y.s.servlet.listener.ScanListener
package com.yap.springboot2.servlet.listener;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;

/**
 * @author yap
 */
@WebListener
public class ScanListener implements ServletContextListener {
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        System.out.println("ScanListener contextInitialized()...");
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        System.out.println("ScanListener contextDestroyed()...");
    }
}

1.2 同时配置方案

流程:

  • 开发原生servlet类 c.y.s.servlet.ContextServlet,无需添加注解。
  • 开发原生过滤器类 c.y.s.servlet.filter.ContextFilter,无需添加注解。
  • 开发原生监听器类 c.y.s.servlet.listener.ContextListener,无需添加注解。
  • 让启动类实现 ServletContextInitializer 接口并重写 onStartup()
    • context.addServlet().addMapping():配置servlet类并设置路由。
    • context.addFilter().addMappingForUrlPatterns():配置过滤器类并设置拦截规则。
    • context.addListener():配置监听器类。
  • psm测试:/api/servlet/context

概念: /springboot/

  • res:c.y.s.servlet.ContextServlet
package com.yap.springboot2.servlet;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author yap
 */
public class ContextServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().write("This is ContextServlet!");
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}
  • res:c.y.s.servlet.filter.ContextFilter
package com.yap.springboot2.servlet.filter;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author yap
 */
public class ContextFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) {
        System.out.println("ContextFilter init()...");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;
        System.out.println("ContextFilter doFilter(): " + req.getRequestURI());
        chain.doFilter(req, resp);
    }

    @Override
    public void destroy() {
        System.out.println("ContextFilter destroy()...");
    }

}
  • res:c.y.s.servlet.listener.ContextListener
package com.yap.springboot2.servlet.listener;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

/**
 * @author yap
 */
public class ContextListener implements ServletContextListener {
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        System.out.println("ContextListener contextInitialized()...");
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        System.out.println("ContextListener contextDestroyed()...");
    }
}
  • res:c.y.s.Springboot2Application
    @Override
    public void onStartup(ServletContext context) {

        context.addServlet("contextServlet", new ContextServlet())
                .addMapping("/api/servlet/contextServlet");

        context.addFilter("contextFilter", new ContextFilter())
                .addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true,
                        "/api/servlet/*");

        context.addListener(new ContextListener());
    }

2. Spring拦截器

流程:

  • 配置pom依赖 spring-boot-starter-aop
  • 开发切面配置类 c.y.s.aop.AopAspect:标记 @Aspect@Configuration
    • 开发通知方法:如环绕通知 @Around("execution()")
  • 开发动作类 c.y.s.aop.AopController
  • psm测试:/api/aop/execute

概念: /springboot/

  • res:pom.xml
   <!--spring-boot-starter-aop-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
  • src:c.y.s.aop.AopAspect
package com.yap.springboot2.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.context.annotation.Configuration;

/**
 * @author yap
 */
@Aspect
@Configuration
public class AopAspect {

    @Around("execution(* com.yap.springboot2.aop..*(..))")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("aop: before...");
        Object result = null;
        try {
            // 放行
            result = pjp.proceed(pjp.getArgs());
            System.out.println("aop: after-returning...");
        } catch (Throwable e) {
            System.out.println("aop: throwing...");
        }

        System.out.println("aop: after...");
        return result;
    }
}
  • src:c.y.s.aop.AopController
package com.yap.springboot2.aop;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author yap
 */
@RestController
@RequestMapping("/api/aop")
public class AopController {

    @RequestMapping("execute")
    public String execute(Integer meta) {
        if (meta == 0) {
            throw new RuntimeException("execute() exception...");
        }
        System.out.println("execute()...");
        return "success";
    }
}

3. 全局异常

概念: 全局异常处理类的优先级低于异常通知,二者共存时,可以在异常通知处理中直接return错误信息,同样会返回给B端:

  • 开发全局异常处理类 c.y.s.exception.GlobalException:标记 @ControllerAdvice
    • 异常处理方法标记 @ExceptionHandler 以指定捕获哪些异常。
    • 异常处理方法标记 @ResponseBody 以返回异常信息内容。
  • 开发动作类 c.y.s.exception.ExceptionController:开发一个会爆发异常的动作方法。
  • psm测试:/api/exception/execute概念: /springboot/
  • src:c.y.s.exception.GlobalException
package com.yap.springboot2.exception;

import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.HashMap;
import java.util.Map;

/**
 * @author yap
 */
@ControllerAdvice
public class GlobalException {

    @ExceptionHandler(Exception.class)
    @ResponseBody
    public Map<String, Object> exception(Exception e) {
        System.out.println("GlobalException.exception()...");
        Map<String, Object> exceptionMsg = new HashMap<>(2);
        exceptionMsg.put("status", 500);
        exceptionMsg.put("msg", e.getMessage());
        return exceptionMsg;
    }
}
  • src:c.y.s.exception.ExceptionController
package com.yap.springboot2.exception;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author yap
 */
@RestController
@RequestMapping("/api/exception")
public class ExceptionController {

    @RequestMapping("/execute")
    public String execute(Integer meta) {
        if (meta == 0) {
            throw new RuntimeException("execute() exception...");
        }
        return "success";
    }
}

4. 定时任务

概念: springboot内置 @EnableScheduling 以替代 java.util.Timer/TimerTask 完成定时任务:

  • 启动类标记 @EnableScheduling 以开启定时功能。
  • 开发任务类 c.y.s.schedule.ScheduleTask:标记 @Component 以被spring管理。
  • 开发任务方法并标记 @Scheduled
    • cronCRON表达式
    • fixedDelay=1000:立即执行任务,每次任务完成后计时,每隔1秒执行一次任务。
    • fixedRate=1000:立即执行任务,每次任务开始前计时,每隔1秒执行一次任务。
  • 启动入口类,控制台观测任务执行情况。 概念: /springboot/
  • src:c.y.s.schedule.ScheduleTask
package com.yap.springboot2.schedule;

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.concurrent.TimeUnit;

/**
 * @author yap
 */
@Component
public class ScheduleTask {

    @Scheduled(fixedDelay = 30000)
    public void printDate() throws InterruptedException {
        TimeUnit.SECONDS.sleep(20L);
        System.out.println("current date: " + new Date());
    }
}

5. 异步处理

概念: 异步执行方法不占用主线程资源,可以提高项目执行效率:

  • 启动类上添加 @EnableAsync 开启异步功能。
  • 开发任务类 c.y.s.async.AsyncTask:标记 @Component 被spring管理。
  • 开发三个任务方法,分别模拟耗时1s/2s/3s:标记 @Async 以异步调用:
    • 若任务类中所有方法都是异步调用,则可将 @Async 标记在类上。
    • 方法返回值可封装在实现了 Future 接口的 AsyncResult 类中。
  • 开发动作类 c.y.s.async.AsyncController:依次调用任务方法:
    • 对返回值调用 get() 可以获取任务方法的返回值。
    • 对返回值调用 isDone() 可以判断任务是否已完成。
  • psm:api/async/execute概念: /springboot/
  • src:c.y.s.async.AsyncTask
package com.yap.springboot2.async;


import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.stereotype.Component;

import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

/**
 * @author yap
 */
@Component
public class AsyncTask {

    @Async
    public Future<String> taskA() {
        try {
            long start = System.currentTimeMillis();
            TimeUnit.SECONDS.sleep(1L);
            long end = System.currentTimeMillis();
            System.out.println("taskA spend: " + (end - start));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return new AsyncResult<>("taskA done...");
    }

    @Async
    public Future<String> taskB() {
        try {
            long start = System.currentTimeMillis();
            TimeUnit.SECONDS.sleep(2L);
            long end = System.currentTimeMillis();
            System.out.println("taskB spend: " + (end - start));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return new AsyncResult<>("taskB done...");
    }

    @Async
    public Future<String> taskC() {
        try {
            long start = System.currentTimeMillis();
            TimeUnit.SECONDS.sleep(3L);
            long end = System.currentTimeMillis();
            System.out.println("taskC spend: " + (end - start));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return new AsyncResult<>("taskC done...");
    }
}
  • src:c.y.s.async.AsyncController
package com.yap.springboot2.async;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

/**
 * @author yap
 */
@RestController
@RequestMapping("api/async")
public class AsyncController {

    private AsyncTask asyncTask;

    @Autowired
    public AsyncController(AsyncTask asyncTask) {
        this.asyncTask = asyncTask;
    }

    @RequestMapping("execute")
    public String execute() throws ExecutionException, InterruptedException {
        long start = System.currentTimeMillis();
        Future<String> taskA = asyncTask.taskA();
        Future<String> taskB = asyncTask.taskB();
        Future<String> taskC = asyncTask.taskC();
        while (true) {
            if (taskA.isDone() && taskB.isDone() && taskC.isDone()) {
                System.out.println("all task is done...");
                System.out.println("taskA return: " + taskA.get());
                System.out.println("taskB return: " + taskB.get());
                System.out.println("taskC return: " + taskC.get());
                break;
            }
        }
        long end = System.currentTimeMillis();
        return "total spend:" + (end - start);
    }
}

6. 响应式编程

概念: webflux 是Spring5中的异步非阻塞响应式编程框架,不依赖servlet,不能部署为war包,不使用webapp目录,请求响应对象使用 ServletRequest/ServletResponse

  • 响应式编程:可利用较少的线程数或硬件资源来处理任务,提高系统的伸缩性,但不会让程序运行的更快:

    • 响应式编程举例.md
  • 创建springboot-jar项目 springboot2-webflux,选择 Web/Spring Reactive Web 依赖:

    • 手动配置需要添加pom依赖 spring-boot-starter-webflux/reactor-test
    • spring-boot-starter-webfluxspring-boot-starter-web 优先级低,共存时失效。
  • 启动入口类:启动方式由tomcat同步容器变为netty异步容器时表示成功引入webflux。

  • 开发实体类 c.y.s.pojo.User

  • 开发业务接口 c.y.s.service.UserService

    • 使用 Mono<User> 异步不阻塞返回单条User数据。
    • 使用 Flux<User> 异步不阻塞返回多条User数据。
  • 开发业务类 c.y.s.service.impl.UserServiceImpl

    • Mono.just(user):返回 Mono 对象,参数为null或空时抛异常。
    • Mono.justOrEmpty(user):返回 Mono 对象,参数为null或空时返回 MonoEmpty 对象。
    • Flux.fromIterable(users):返回 Flux 集合对象。
    • Flux.fromArray(users):返回 Flux 数组对象。
  • 开发动作类 c.y.s.controller.UserController:动作方法可直接返回 Mono/Flux 对象。

    • @RequestMaping 配置 produces="application/stream+json" 可以开启流响应。
    • 对Flux数据调用 delayElements(Duration.ofSeconds(2)) 可以设置响应流速为2秒一次。
  • psm测试 api/webflux/select-by-id

  • cli测试 api/webflux/select-all,postman无法观察流响应效果。

  • 开发WebClient类 c.y.s.user.UserTest:用于模拟发送webflux请求,必须先手动启动项目后测试:

    • WebClient.create("uri").get()/post():创建并配置webflux的get/post请求。
    • retrieve().bodyToMono/Flux(User.class):将User数据提取到Mono/Flux对象中。
    • mono.block():立刻取出Mono中的数据。
    • flux.collectList().block():将Flux中的数据收集到list中。 源码: /springboot-webflux/
  • src:c.y.s.pojo.User

package com.yap.springboot2webflux.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

/**
 * @author yap
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
    private Integer id;
    private String name;
}
  • src:c.y.s.service.UserService
package com.yap.springboot2webflux.service;

import com.yap.springboot2webflux.pojo.User;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

/**
 * @author yap
 */
public interface UserService {
    /**
     * 通过主键查询用户记录
     *
     * @param id 主键
     * @return 满足条件的唯一记录
     */
    Mono<User> selectById(Integer id);

    /**
     * 查询全部用户记录
     *
     * @return 全部用户记录
     */
    Flux<User> selectAll();

}
  • src:c.y.s.service.impl.UserServiceImpl
package com.yap.springboot2webflux.service.impl;

import com.yap.springboot2webflux.pojo.User;
import com.yap.springboot2webflux.service.UserService;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.util.ArrayList;
import java.util.List;

/**
 * @author yap
 */
@Service
public class UserServiceImpl implements UserService {

    private static List<User> users;

    static {
        users = new ArrayList<>();
        users.add(new User(1, "zhaosi"));
        users.add(new User(2, "liuneng"));
        users.add(new User(3, "dajiao"));
    }

    @Override
    public Mono<User> selectById(Integer id) {
        User user = null;
        for (User e : users) {
            if (e.getId().equals(id)) {
                user = e;
                break;
            }
        }
        return Mono.justOrEmpty(user);
    }

    @Override
    public Flux<User> selectAll() {
        return Flux.fromIterable(users);
    }
}
  • src:c.y.s.controller.UserController
package com.yap.springboot2webflux.controller;

import com.yap.springboot2webflux.pojo.User;
import com.yap.springboot2webflux.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.time.Duration;

/**
 * @author yap
 */
@RestController
@RequestMapping("/api/webflux")
public class UserController {

    private UserService userService;

    @Autowired
    public UserController(UserService userService) {
        this.userService = userService;
    }

    @RequestMapping("/select-by-id")
    public Mono<User> selectById(Integer id) {
        return userService.selectById(id);
    }

    @RequestMapping(value = "/select-all", produces = "application/stream+json")
    public Flux<User> selectAll() {
        return userService.selectAll().delayElements(Duration.ofSeconds(2));
    }

}
  • src:c.y.s.user.UserTest
package com.yap.springboot2webflux.user;

import com.yap.springboot2webflux.pojo.User;
import org.junit.jupiter.api.Test;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

/**
 * @author yap
 */
class UserTest {

    @Test
    void webClientMono() {
        /*Mono<User> userMono = WebClient.create()
                .get().uri("http://localhost:8080/api/webflux/select-by-id?id={id}", 1)
                .retrieve().bodyToMono(User.class);*/

        Mono<User> userMono = WebClient.create("http://localhost:8080/api/webflux/select-by-id?id=1")
                .get().retrieve().bodyToMono(User.class);
        System.out.println(userMono.block());
    }

    @Test
    void webClientFlux() {
        Flux<User> userFlux = WebClient
                .create("http://localhost:8080/api/webflux/select-all")
                .get().retrieve().bodyToFlux(User.class);
        System.out.println(userFlux.collectList().block());
    }
}

7. JWT校验

概念: Json Web Token 是一种基于JSON的高效,简洁,安全的开放标准,使用一个 token进行通信,常用于登录鉴权,单点登录等分布式场景:

  • token结构:header.payload.signature
    • header:token头,描述使用哪种加密算法,如MD5,SHA,HMAC等。
    • payload:token负载,可携带部分用户信息以避免多次查库:
      • z-res/jwt内置负载项.md
    • signature:token签名,利header中声明的加密算法配合秘钥secretKey(保存在S端)生成:
      • HMAC(base64(header).base64(payload), secretKey)
  • jwt校验流程:
    • B端首次登录成功时,S端将某些用户信息组合加密成一个token返回B端。
    • B端将该token保存在cookie,sessionStorage或localStorage中。
    • 后续B端请求均携带此token,S端解密成功时执行请求,否则阻止。

流程: 新建项目 springboot2-jwt

  • 配置pom依赖:java-jwt

  • 开发工具类 c.y.s.util.TokenUtil

    • JWT.create():返回一个可配置的 Builder 对象。
    • builder.withClaim(K, V):设置payload中的自定义键值对。
    • builder.sign(Algorithm.HMAC256(secretKey)):通过指定算法和秘钥设置签名。
    • JWT.require(Algorithm.HMAC256(secretKey)).build():通过指定算法和秘钥获取token验证对象。
  • 开发注解 c.y.s.annotation.TokenAuth:标记此注解的方法会进行token验证。

  • 开发业务方法 c.y.s.service.UserService/UserServiceImpl.login():登陆成功返回 User 实例。

  • 开发动作方法 c.y.s.controller.UserController.login()

  • 开发动作方法 c.y.s.controller.UserController.execute():标记 @TokenAuth

  • 开发拦截器 c.y.s.interceptor.AuthInterceptor:重写 HandlerInterceptor.preHandle()

    • JWT.decode(token).getClaim(K).asString():解码token并获取指定的payload值并转成字符串。
    • JWT.decode(token).getClaim(K).asInt():解码token并获取指定的payload值并转成整型。
  • 开发配置类 c.y.s.config.AuthInterceptorConfig:重写 WebMvcConfigurer.addInterceptors()

    • registry.addInterceptor():添加自定义拦截器类实例。
    • registry.addPathPatterns():添加拦截器拦截规则。
    • registry.excludePathPatterns():添加拦截器排除规则。
  • psm: api/user/execute,不加token,登录失败,控制台抛出异常。

  • psm:api/user/login 正常登录,返回token。

  • psm:api/user/execute,请求头携带token,通过验证。 源码: :/springboot2-jwt/

  • pom:pom.xml

		<!--java-jwt-->
		<dependency>
			<groupId>com.auth0</groupId>
			<artifactId>java-jwt</artifactId>
			<version>3.4.0</version>
		</dependency>
  • src:c.y.s.util.TokenUtil
package com.yap.springboot2jwt.util;


import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.yap.springboot2jwt.entity.User;

import java.util.Date;

/**
 * @author yap
 */
public class TokenUtil {

    /**
     * token令牌过期时间,单位毫秒
     */
    private static final long EXPIRE = 1000 * 60 * 60 * 24 * 7;

    /**
     * token令牌秘钥
     */
    private static final String SECRET_KEY = "my-secret";

    /**
     * token令牌发行者
     */
    private static final String ISSUER = "yap";

    /**
     * token令牌主题
     */
    private static final String SUBJECT = "login auth";

    public static String build(User user){
        return JWT.create()
                .withClaim("id", user.getId())
                .withClaim("username", user.getUsername())
                .withClaim("avatar", user.getAvatar())
                .withSubject(SUBJECT)
                .withIssuer(ISSUER)
                .withIssuedAt(new Date())
                .withExpiresAt(new Date(System.currentTimeMillis() + EXPIRE))
                .sign(Algorithm.HMAC256(SECRET_KEY));
    }

    public static DecodedJWT verify(String token){
        try{
            JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(SECRET_KEY)).build();
            try {
                return jwtVerifier.verify(token);
            } catch (JWTVerificationException e) {
                throw new RuntimeException("token verify error...");
            }
        }catch (Exception e){
            return null;
        }
    }
}

  • src:c.y.s.annotation.TokenAuth
package com.yap.springboot2jwt.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author yap
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TokenAuth {
}
  • src:c.y.s.service.UserService
package com.yap.springboot2jwt.service;

import com.yap.springboot2jwt.entity.User;

/**
 * @author yap
 */
public interface UserService {

    /**
     * 登录
     *
     * @param user 用户实体
     * @return 对应用户的一条记录
     */
    User login(User user);
}
  • src:c.y.s.service.impl.UserServiceImpl.login()
package com.yap.springboot2jwt.service.impl;

import com.yap.springboot2jwt.entity.User;
import com.yap.springboot2jwt.service.UserService;
import org.springframework.stereotype.Service;

/**
 * @author yap
 */
@Service
public class UserServiceImpl implements UserService {

    @Override
    public User login(User user) {
        String username, password;
        if (user == null || (username = user.getUsername()) == null || (password = user.getPassword()) == null) {
            return null;
        }

        String usernameFromDb = "admin";
        String passwordFromDb = "123";
        if (usernameFromDb.equals(username) && passwordFromDb.equals(password)) {
            return new User(1, "admin", "123", "admin.jpg");
        } else {
            return null;
        }
    }
}
  • src:c.y.s.controller.UserController.login()and.execute()
package com.yap.springboot2jwt.controller;

import com.yap.springboot2jwt.annotation.TokenAuth;
import com.yap.springboot2jwt.entity.User;
import com.yap.springboot2jwt.service.UserService;
import com.yap.springboot2jwt.util.TokenUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author yap
 */
@RestController
@RequestMapping("/api/user")
public class UserController {

    private UserService userService;

    @Autowired
    public UserController(UserService userService) {
        this.userService = userService;
    }

    @RequestMapping("login")
    public String login(User user) {
        User userFromDb = userService.login(user);
        if (userFromDb != null) {
            return TokenUtil.build(userFromDb);
        }
        return "login fail...";
    }

    @TokenAuth
    @RequestMapping("execute")
    public String execute() {
        return "execute()...";
    }
}
  • src:c.y.s.interceptor.AuthInterceptor
package com.yap.springboot2jwt.interceptor;

import com.auth0.jwt.interfaces.DecodedJWT;
import com.yap.springboot2jwt.annotation.TokenAuth;
import com.yap.springboot2jwt.util.TokenUtil;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Method;

/**
 * @author yap
 */
@Component
public class AuthInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object obj) throws IOException {

        // 若请求URL不指向动作方法,直接放行
        if (!(obj instanceof HandlerMethod)) {
            return true;
        }

        // 若动作方法上没有标记@TokenAuth,直接放行
        Method method = ((HandlerMethod) obj).getMethod();
        if (!method.isAnnotationPresent(TokenAuth.class)) {
            return true;
        }

        // 从请求头中获取token,若没获取成功,尝试从请求参数中获取,若均失败返回错误。
        String token = req.getHeader("token");
        if (token == null || "".equals(token)) {
            token = req.getParameter("token");
            if (token == null || "".equals(token)) {
                throw new RuntimeException("token is null...");
            }
        }

        DecodedJWT decodedJwt = TokenUtil.verify(token);
        if (decodedJwt == null) {
            throw new RuntimeException("token verify error...");
        }
        int id = decodedJwt.getClaim("id").asInt();
        String username = decodedJwt.getClaim("username").asString();
        String avatar = decodedJwt.getClaim("avatar").asString();
        req.setAttribute("id", id);
        req.setAttribute("username", username);
        req.setAttribute("avatar", avatar);
        return true;
    }
}
  • src:c.y.s.config.AuthInterceptorConfig
package com.yap.springboot2jwt.config;

import com.yap.springboot2jwt.interceptor.AuthInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @author yap
 */
@Configuration
public class AuthInterceptorConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new AuthInterceptor())
                .addPathPatterns("/api/**")
                .excludePathPatterns("/api/user/login");
    }
}

jwt内置负载项

概念: jwt内置负载项:

标识符全称概述备注
ississuertoken发行人可由 builder.withIssuer() 设置
iatissued attoken发行时间可由 builder.withIssuedAt() 设置
subsubjecttoken主题内容可由 builder.withSubject() 设置
audaudiencetoken接收人可由 builder.withAudience() 设置
expexpirationtoken过期时间戳可由 builder.withExpiration() 设置
nbfnot beforetoken生效时间戳可由 builder.withNotBefore() 设置
jtijwt idtoken的id可由 builder.withJWTId() 设置

jwt自写工具类

package com.yap.z11springboot2.jwt;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.util.Date;

/**
 * @Author Yap
 */
public class JwtUtil {
    /**
     * 密钥:不能暴露
     */
    private static String secretKey = "wa134679";

    public static String geneToken(User user) {
        Integer id;
        String username, avatar;

        if (user == null || (id = user.getId()) == null || (username = user.getUsername()) == null || (avatar = user.getAvatar()) == null) {
            return null;
        }
        // 设置负载(发行人,发行时间,过期时间)和签名
        // 每个claim对象都对应payload中的一个kv对
        return Jwts.builder()
                .claim("id", id)
                .claim("username", username)
                .claim("avatar", avatar)
                .setSubject("yap")
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + 1000 * 3600 * 24L))
                .signWith(SignatureAlgorithm.HS256, secretKey)
                .compact();
    }

    public static Claims checkToken(String token) {
        final Claims claims;
        try {
            // 获取payload
            claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
        return claims;
    }

}

  • test
package com.yap.z11springboot2.jwt;

import io.jsonwebtoken.Claims;
import org.junit.jupiter.api.Test;

/**
 * @Author Yap
 */
class JwtTest {

    @Test
    void geneJwt() {
        System.out.println(JwtUtil.geneToken(new User(1, "qaz129", "wa67890", "yap.jpg")));
    }

    @Test
    void checkJwt() {
        // 将上一个方法生成的token拿过来
        // 这个字符串稍微改动一点都会返回一个null的Claims
        String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwYXNzd29yZCI6IjEyMyIsInVzZXJuYW1lIjoiYWRtaW4ifQ.P6fnBFeFf01_vLBMzMrsG_5PidZSop5lry3You4yRIk";
        Claims claims = JwtUtil.checkToken(token);
        if (claims != null) {
            //System.out.println(claims.get("id"));
            System.out.println(claims.get("username"));
            System.out.println(claims.get("password"));
        }else {
            System.out.println("illegal token...");
        }
    }
}