解决子线程中获取不到HttpServletRequest对象的问题
作者:沐雨橙风ιε
这篇文章主要介绍了解决子线程中获取不到HttpServletRequest对象的问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
子线程中获取不到HttpServletRequest对象
本文主要分享一下项目里遇到的获取request对象为null的问题,具体是在登录的时候触发的邮箱提醒,获取客户端ip地址,然后通过ip地址定位获取定位信息,从而提示账号在哪里登录。
但是登录却发现获取request对象的时候报错了。
具体的代码
如下:这个异常是自己手动抛出的。
package cn.edu.sgu.www.mhxysy.util; import cn.edu.sgu.www.mhxysy.consts.MimeType; import cn.edu.sgu.www.mhxysy.exception.GlobalException; import cn.edu.sgu.www.mhxysy.restful.JsonResult; import cn.edu.sgu.www.mhxysy.restful.ResponseCode; import com.alibaba.fastjson.JSON; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * http工具类 * @author heyunlin * @version 1.0 */ public class HttpUtils { /** * 获取HttpServletRequest对象 * @return HttpServletRequest */ public static HttpServletRequest getRequest() { RequestAttributes attributes = RequestContextHolder.getRequestAttributes(); if (attributes != null ) { return ((ServletRequestAttributes) attributes).getRequest(); } throw new GlobalException(ResponseCode.ERROR, "获取request对象失败"); } }
在项目其他地方也有用这个工具了获取HttpServletRequest对象,都能获取到,觉得很是奇怪。
点进去RequestContextHolder这个类的代码里看了一下,好像找到问题了~
这是基于ThreadLocal实现的,可能与子线程无法访问父线程中设置的数据的问题有关。
- 不会ThreadLocal的童鞋,通过下面文章简单了解一下ThreadLocal
- 子线程无法访问父线程中通过ThreadLocal设置的变量
为了验证自己的猜测,点开RequestContextHolder的源代码~
public abstract class RequestContextHolder { private static final ThreadLocal<RequestAttributes> requestAttributesHolder = new NamedThreadLocal<>("Request attributes"); private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder = new NamedInheritableThreadLocal<>("Request context"); /** * 根据inheritable的值决定通过ThreadLocal或InhertitableThreadLocal保存RequestAttributes对象 */ public static void setRequestAttributes(@Nullable RequestAttributes attributes, boolean inheritable) { if (attributes == null) { resetRequestAttributes(); } else { if (inheritable) { inheritableRequestAttributesHolder.set(attributes); requestAttributesHolder.remove(); } else { requestAttributesHolder.set(attributes); inheritableRequestAttributesHolder.remove(); } } } /** * 通过Ctrl+鼠标点击,发现实际调用的是这个方法 * 所以默认是通过ThreadLocal保存的变量 */ public static void setRequestAttributes(@Nullable RequestAttributes attributes) { setRequestAttributes(attributes, false); } /** * 获取ThreadLocal中设置的RequestAttributes对象 */ @Nullable public static RequestAttributes getRequestAttributes() { // 因为默认是通过ThreadLocal而不是InheritableThreadLocal保存, // 在子线程访问不到在父线程中通过set()方法设置的变量 RequestAttributes attributes = requestAttributesHolder.get(); // 所以在子线程中,会走这个分支 if (attributes == null) { // 然后从InheritableThreadLocal中获取RequestAttributes对象 // 因为调用上面第一个setRequestAttributes(RequestAttributes, boolean)方法时传的参数是false,所以InheritableThreadLocal中没有设置RequestAttributes对象,因此,这里get()还是null,最后attributes的值为null attributes = inheritableRequestAttributesHolder.get(); } return attributes; } }
于是,把涉及获取request对象ip地址获取的代码放在线程外面,这样就避免了空指针问题了~
package cn.edu.sgu.www.mhxysy.chain.login.impl; import cn.edu.sgu.www.mhxysy.chain.login.UserLoginHandler; import cn.edu.sgu.www.mhxysy.config.property.EmailProperties; import cn.edu.sgu.www.mhxysy.config.property.SystemSettingsProperties; import cn.edu.sgu.www.mhxysy.entity.location.Location; import cn.edu.sgu.www.mhxysy.util.EmailUtils; import cn.edu.sgu.www.mhxysy.util.IpUtils; import cn.edu.sgu.www.mhxysy.util.LocationUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.format.DateTimeFormatter; /** * @author heyunlin * @version 1.0 */ @Component public class EmailSendHandler implements UserLoginHandler { private Object params; private UserLoginHandler next; private final EmailUtils emailUtils; private final EmailProperties emailProperties; private final SystemSettingsProperties systemSettingsProperties; @Autowired public EmailSendHandler( EmailUtils emailUtils, EmailProperties emailProperties, SystemSettingsProperties systemSettingsProperties) { this.emailUtils = emailUtils; this.emailProperties = emailProperties; this.systemSettingsProperties = systemSettingsProperties; } @Override public void handle() { if (emailProperties.isEnable()) { String ip = IpUtils.getIp(); String username = (String) params; String zoneId = systemSettingsProperties.getZoneId(); // 定义日期格式 DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss"); new Thread(() -> { try { String address = "广东广州"; Location location = LocationUtils.getLocation(ip); if (systemSettingsProperties.isUseRealLocation()) { String locationAddress = location.getAddress(); if (locationAddress != null) { address = locationAddress; } } String text = "您的账号" + username + "在" + address + "登录了。" + "[" + LocalDateTime.now(ZoneId.of(zoneId)).format(formatter) + "]"; emailUtils.sendMessage(text); } catch (Exception e) { e.printStackTrace(); } }).start(); } if (next != null) { next.handle(); } } @Override public void setNext(UserLoginHandler next) { this.next = next; } @Override public void setParams(Object params) { this.params = params; } }
总结
遇到这类问题,就把获取request对象的代码放在主线程中,避免因为ThreadLocal的问题导致程序异常。
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。