关于如何优雅地获取通用请求头参数这件事--构建Http上下文,避免一传到底

1,640 阅读6分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

1 序言

优雅的代码让人阅读起来如沐春风,糟糕的代码阅读起来痛不欲生.对于获取通用请求头参数,比如token、userId、deviceId,相信各位程序员小伙伴再熟悉不过了.接下来,我们就聊聊如何写好这份代码.

2 牛刀小试

"Talk is cheap. Show me the code"Linux如是说,接下来我们比较几种获取请求头参数的方式.孰优孰劣,比较便知.

首先,我们尝试通过三种不同的方式获取到请求中的deviceId,分别为@RequestHeader、HttpServletRequest、AppContext,下面为具体代码.

2.1 @RequestHeader

代码

@RestController
@RequestMapping("/user")
public class UserController {

    @GetMapping("getRequestHeader")
    public ApiResult getRequestHeader(@RequestHeader String deviceId) {
        return ApiResult.success(deviceId);
    }

}

测试结果

image.png

2.2 HttpServletRequest

代码

@RestController
@RequestMapping("/user")
public class UserController {

    @GetMapping("getByHttpServletRequest")
    public ApiResult getByHttpServletRequest(HttpServletRequest request) {
        return ApiResult.success(request.getHeader("deviceId"));
    }

}

测试结果

image.png

2.3 AppContext

代码

@RestController
@RequestMapping("/user")
public class UserController {

    @GetMapping("getByAppContext")
    public ApiResult getByAppContext() {
        return ApiResult.success(AppContextUtil.getDeviceId());
    }

}

测试结果

image.png

小结:三种方式都能很好的完成功能

3 需求升级

三种方式的确都能正确地完成功能,但当我们需要在service新增一种请求头参数时,你会发现前两种方式都需要改动方法签名,而第三种方式却不需要.相对前两种,更直接更简洁.接下来,给出一串传递userId和deviceId的代码,大家可以比较一下差异.

controller层代码

@RestController
@RequestMapping("/user")
public class UserController {

    @Resource
    UserService userService;


    @GetMapping("getByRequestHeader")
    public ApiResult getByRequestHeader(@RequestHeader String deviceId, @RequestHeader String userId) {
        userService.testRequestHeader(userId, deviceId);
        return ApiResult.success();
    }

    @GetMapping("getByHttpServletRequest")
    public ApiResult getByHttpServletRequest(HttpServletRequest request) {
        userService.testHttpServletRequest(request);
        return ApiResult.success();
    }

    @GetMapping("getByAppContext")
    public ApiResult getByAppContext() {
        userService.testAppContext();
        return ApiResult.success();
    }

}

Service层代码

@Service
@Slf4j
public class UserServiceImpl implements UserService {


    @Override
    public void testRequestHeader(String userId, String deviceId) {
        log.info("testRequestHeader: {},{}", userId, deviceId);
    }

    @Override
    public void testHttpServletRequest(HttpServletRequest request) {
        log.info("testHttpServletRequest: {},{}", request.getHeader("userId"), request.getHeader("deviceId"));
    }

    @Override
    public void testAppContext() {
        log.info("testHttpServletRequest: {},{}", AppContextUtil.getUserId(), AppContextUtil.getDeviceId());
    }
}

测试结果

2022-04-13 23:34:15.031  INFO 5484 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2022-04-13 23:34:15.061  INFO 5484 --- [           main] xyz.yq56.admin.common.config.MvcConfig   : 添加拦截器: xyz.yq56.admin.interceptor.AppContextInterceptor@561b7d53
2022-04-13 23:34:15.222  INFO 5484 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2022-04-13 23:34:15.228  INFO 5484 --- [           main] xyz.yq56.admin.AdminApplication          : Started AdminApplication in 2.271 seconds (JVM running for 2.89)
2022-04-13 23:34:15.261  INFO 5484 --- [           main] x.y.admin.runner.init.CommonInitRunner   : ApplicationRunner: {"sourceArgs":[],"optionNames":[],"nonOptionArgs":[]}
2022-04-13 23:34:15.261  INFO 5484 --- [           main] x.y.admin.runner.init.CommonInitRunner   : CommandLineRunner: []
2022-04-13 23:34:21.651  INFO 5484 --- [nio-8080-exec-2] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2022-04-13 23:34:21.651  INFO 5484 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2022-04-13 23:34:21.657  INFO 5484 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet        : Completed initialization in 6 ms
2022-04-13 23:34:21.683  INFO 5484 --- [nio-8080-exec-2] x.y.a.m.u.service.impl.UserServiceImpl   : testRequestHeader: userId,deviceId-yq-1234-dadf
2022-04-13 23:34:28.796  INFO 5484 --- [nio-8080-exec-3] x.y.a.m.u.service.impl.UserServiceImpl   : testHttpServletRequest: userId,deviceId-yq-1234-dadf
2022-04-13 23:34:34.102  INFO 5484 --- [nio-8080-exec-4] x.y.a.m.u.service.impl.UserServiceImpl   : testHttpServletRequest: userId,deviceId-yq-1234-dadf

总结

1.上下文方式不需要给每个方法都添加参数,相对更为简洁灵活

2.新增参数时,上下文方式不需要改动方法签名,代码扩展性也更强

4 构建步骤

相信经过对比,第三种方式优势还是比较明显的,那么该如何操作呢?

首先讲思路,通过拦截器获取到请求参数,然后统一封装到自定义的AppContext,并将上下文对象保存到HttpServletRequest中(这里使用ThreadLocal也是可行的,不过使用完后要注意清除ThreadLocal的数据,防止内存溢出),最后通过工具类封装HttpServletRequest方便外部调用.

4.1 代码实现

4.1.1 定义上下文对象,这里自定义即可

package common;

import lombok.Data;

/**
 * 应用上下文
 *
 * @author yi qiang
 * @date 2022/4/4 14:19
 */
@Data
public class AppContext {

    /**
     * 身份令牌
     */
    private String token;

    /**
     * 用户IP
     */
    private String ip;

    /**
     * 设备ID
     */
    private String deviceId;

    /**
     * 用户ID
     */
    private String userId;

    /**
     * 语言
     */
    private String language;

}

4.1.2 编写拦截器,并注册到拦截器链中

  1. 编写拦截器: 填充上下文对象的值,并保存到HttpServletRequest中
//我这里对HandlerInterceptorAdapter封装了一层,方便找到自定义的拦截器
public class PmsInterceptor extends HandlerInterceptorAdapter {
}

@Component
@Slf4j
public class AppContextInterceptor extends PmsInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        AppContext appContext = new AppContext();
        appContext.setDeviceId(RequestUtil.getHeaderOrQueryParam(request, "deviceId"));
        appContext.setIp(RequestUtil.getRealIp());
        appContext.setToken(RequestUtil.getHeaderOrQueryParam(request, "token"));
        appContext.setLanguage(RequestUtil.getHeaderOrQueryParam(request, "language"));
        appContext.setUserId(RequestUtil.getHeaderOrQueryParam(request, "userId"));
        request.setAttribute(Constants.APP_CONTEXT, appContext);
        if (log.isDebugEnabled()) {
            log.debug("设置上下文环境: {}", JsonUtil.toJson(appContext));
        }

        return true;
    }

}
  1. 注册到拦截器链中

/**
 * <p>
 * WebMvcConfigurerAdapter的作用是适配WebMvcConfigurer接口,使得调用方不必去实现WebMvcConfigurer的每一个接口方法 <br/>
 * 对于Java 1.8而言,已提供default方法,因而WebMvcConfigurerAdapter已失去他的意义,并已被标记为过时方法
 * </p>
 *
 * @author yi qiang
 * @date 2022/4/4 22:28
 */
@Configuration
@Slf4j
public class MvcConfig implements WebMvcConfigurer {

    @Resource
    List<PmsInterceptor> pmsInterceptorList;

    //此处为添加拦截器方法
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        if (CollectionUtils.isEmpty(pmsInterceptorList)) {
            return;
        }
        for (PmsInterceptor intercept : pmsInterceptorList) {
            log.info("添加拦截器: {}", intercept);
            registry.addInterceptor(intercept);
        }
    }

}

使用到的工具类RequestUtil

/**
 * @author yiqiang
 */
@SuppressWarnings("all")
public class RequestUtil {
    private static final Logger log = LoggerFactory.getLogger(RequestUtil.class);

    private RequestUtil() {
    }

    public static HttpServletRequest getCurrentHttpRequest() {
        return getCurrentHttpRequest(true);
    }

    public static HttpServletRequest getCurrentHttpRequest(boolean warnLog) {
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        if (requestAttributes instanceof ServletRequestAttributes) {
            return ((ServletRequestAttributes) requestAttributes).getRequest();
        } else {
            if (warnLog) {
                log.warn("Not called in the context of an HTTP request");
            }
            return null;
        }
    }

    public static String getFirstXForwardForIp(String xForwardFor) {
        if (StringUtils.isBlank(xForwardFor)) {
            return null;
        } else {
            if (StringUtils.contains(xForwardFor, ",")) {
                String[] data = StringUtils.split(xForwardFor, ",");
                if (data.length > 0) {
                    return StringUtils.trim(data[0]);
                }
            }

            return xForwardFor;
        }
    }

    public static String getRealIp() {
        HttpServletRequest request = getCurrentHttpRequest();
        return getRealIp(request);
    }

    public static String getRealIp(HttpServletRequest request) {
        String remoteAddr = "";
        if (request == null) {
            return remoteAddr;
        } else {
            remoteAddr = request.getHeader("X-Forwarded-For");
            remoteAddr = getFirstXForwardForIp(remoteAddr);
            if (StringUtils.isNotBlank(remoteAddr)) {
                return remoteAddr;
            } else {
                remoteAddr = request.getHeader("X-Real-Ip");
                if (StringUtils.isNotEmpty(remoteAddr)) {
                    return remoteAddr;
                } else {
                    remoteAddr = request.getHeader("RealIP");
                    return StringUtils.isNotEmpty(remoteAddr) ? remoteAddr : request.getRemoteAddr();
                }
            }
        }
    }

    public static String getUserAgent() {
        HttpServletRequest request = getCurrentHttpRequest();
        return request == null ? null : request.getHeader("User-Agent");
    }

    public static String getRequestUrl(NativeWebRequest nativeWebRequest) {
        return getRequestUrl(nativeWebRequest.getNativeRequest(HttpServletRequest.class));
    }

    public static String getRequestUrl(HttpServletRequest request) {
        if (request != null) {
            return StringUtils.isNotBlank(request.getQueryString()) ? request.getRequestURI() + "?" + request.getQueryString() : request.getRequestURI();
        } else {
            return "/unknown-url";
        }
    }

    public static String getHeaderOrQueryParam(HttpServletRequest webRequest, String name) {
        if (webRequest == null) {
            return null;
        }
        String value = webRequest.getHeader(name);
        if (StringUtils.isNotBlank(value)) {
            return value;
        } else {
            value = webRequest.getParameter(name);
            return StringUtils.isNotBlank(value) ? value : null;
        }
    }

    public static String getHeaderOrQueryParam(String name) {
        return getHeaderOrQueryParam(getCurrentHttpRequest(), name);
    }


    public static String getRequestBody() {
        return getRequestBody(getCurrentHttpRequest());
    }

    public static String getRequestBody(HttpServletRequest request) {
        try {
            return request == null ? null : IOUtils.toString(request.getReader());
        } catch (IOException var2) {
            log.error(request.getRequestURI(), var2);
            return null;
        }
    }

    public static Map<String, String> getHeaderMaps() {
        HttpServletRequest currentHttpRequest = getCurrentHttpRequest();
        if (currentHttpRequest == null) {
            return Collections.emptyMap();
        } else {
            Enumeration<String> headerNames = currentHttpRequest.getHeaderNames();
            HashMap<String, String> headerMap = new HashMap<>(8);

            while (headerNames.hasMoreElements()) {
                String name = headerNames.nextElement();
                String value = currentHttpRequest.getHeader(name);
                headerMap.put(name, value);
            }

            return headerMap;
        }
    }

    public static String getHttpHost() {
        HttpServletRequest servletRequest = getCurrentHttpRequest();
        if (servletRequest == null) {
            throw new IllegalArgumentException("not http context");
        } else {
            return servletRequest.getScheme() + "://" + servletRequest.getHeader("HOST");
        }
    }
}

4.1.3 封装上下文工具类AppContextUtil

public class AppContextUtil {

    private AppContextUtil() {
    }

    public static AppContext getAppContext() {
        HttpServletRequest currentHttpRequest = RequestUtil.getCurrentHttpRequest();
        if (currentHttpRequest == null) {
            return new AppContext();
        }
        Object attribute = currentHttpRequest.getAttribute(Constants.APP_CONTEXT);
        return attribute == null ? new AppContext() : (AppContext) attribute;
    }

    public static String getDeviceId() {
        return Optional.ofNullable(getAppContext()).map(AppContext::getDeviceId).orElse(null);
    }

    public static String getUserId() {
        return Optional.ofNullable(getAppContext()).map(AppContext::getUserId).orElse(null);
    }

}

4.2 使用方法

这个示例其实前面已经有了.直接使用AppContextUtil静态方法即可

@Override
public void testAppContext() {
    log.info("testHttpServletRequest: {},{}", AppContextUtil.getUserId(), AppContextUtil.getDeviceId());
}

5 后话

就传参而言,其实还有很多可以探讨的地方.比如传入的token,如何优雅地获取到对应的User对象?加密的参数,怎么优雅地解密?普通程序员可能每次都在Controller里面对token解析出userId,然后使用UserService获取对应的对象等待.但仔细想想,这里使用自定义注解+自定义消息转化器会不会更优雅?

好了,本期分享就到这里.下期再分享自定义消息转化器,敬请关注.