本文已参与「新人创作礼」活动,一起开启掘金创作之路。
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);
}
}
测试结果
2.2 HttpServletRequest
代码
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping("getByHttpServletRequest")
public ApiResult getByHttpServletRequest(HttpServletRequest request) {
return ApiResult.success(request.getHeader("deviceId"));
}
}
测试结果
2.3 AppContext
代码
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping("getByAppContext")
public ApiResult getByAppContext() {
return ApiResult.success(AppContextUtil.getDeviceId());
}
}
测试结果
小结:三种方式都能很好的完成功能
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 编写拦截器,并注册到拦截器链中
- 编写拦截器: 填充上下文对象的值,并保存到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;
}
}
- 注册到拦截器链中
/**
* <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获取对应的对象等待.但仔细想想,这里使用自定义注解+自定义消息转化器会不会更优雅?
好了,本期分享就到这里.下期再分享自定义消息转化器,敬请关注.