一、为什么要用拦截器?
每次请求都要显示头部信息,不同状态头部信息不同:
- 如果登录状态,显示首页、消息、头像
- 如果是未登录状态,显示首页、注册、登录
拦截浏览器的请求,每次有请求,在开始或结束时加代码。
解决通用问题,降低耦合度(拦截器和Controller没有直接的关系,所有耦合度很低。)。
二、拦截器的基本使用、示例
1、定义拦截器,实现HandlerInterceptor
HandlerInterceptor接口有三个方法,default表示不强制实现这个方法
表现层的逻辑,在controller.interceptor下写AlphaInterceptor.java:
@Component
public class AlphaInterceptor implements HandlerInterceptor {
private static final Logger logger = LoggerFactory.getLogger(AlphaInterceptor.class);
//在Controller之前执行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
logger.debug("preHandle: " + handler.toString());
//可以在request和response中加逻辑
return true;//返回false,不往下执行
}
//在Controller之后执行,相当于对Controller最后的补充,所以会给modelAndView
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
logger.debug("postHandle: " + handler.toString());
}
//在模板引擎TemplateEngine之后,最后执行
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
logger.debug("afterCompletion: " + handler.toString());
}
}
2、配置拦截器,为它指定拦截、排除的路径
拦截器和Controller没有直接的关系,所以耦合度很低。
实现WebMvcConfigurer接口的addInterceptors方法,利用传进来的对象注册Interceptor。
注入拦截器,静态资源随意访问,静态资源的路径被排除,要拦截的路径是"/register", "/login"
。
/**
表示上级路径或下级路径。"/**/*.css"
即所有路径下的css文件.
WebMvcConfig.java
@Configuration//配置类,一般是生成第三方bean,这里实现接口
public class WebMvcConfig implements WebMvcConfigurer {
//注入拦截器,给它配置
@Autowired
private AlphaInterceptor alphaInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(alphaInterceptor)
.excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg")
.addPathPatterns("/register", "/login");
}
}
点一下注册、登录,打印的日志如下:
三、该项目下的拦截器应用
1、实现拦截器LoginTicketInterceptor
每次请求都要进行的操作(所以要用拦截器Interceptor):
- 通过cookie获取ticket (preHandle)
- 通过ticket获取loginTicket凭证实例 (preHandle)
- 通过loginTicket获取user实例,并用ThreadLocal保存user,保证并发时线程隔离 (preHandle)
- 对user实例进行一系列controller操作
- 给模板引擎Template (postHandle)
- 线程退出,清除该user的登录状态 (afterCompletion)
LoginTicketInterceptor.java
@Component
public class LoginTicketInterceptor implements HandlerInterceptor {
@Autowired
private UserService userService;
@Autowired
private HostHolder hostHolder;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 从request获取name为ticket的cookie value
String ticket = CookieUtil.getValue(request, "ticket");
if (ticket != null) {
// 查询凭证实例oginTicket
LoginTicket loginTicket = userService.findLoginTicket(ticket);
// 检查凭证是否有效、登出、过期时间晚于当前时间
if (loginTicket != null && loginTicket.getStatus() == 0 && loginTicket.getExpired().after(new Date())) {
// 根据凭证查询用户实例user loginTicket->userId->user
User user = userService.findUserById(loginTicket.getUserId());
hostHolder.setUsers(user);// 在本次请求中持有用户,在服务器中存,并发,线程隔离用ThreadLocal
}
}
return true;//如果是false后面就不执行了
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
User user = hostHolder.getUsers();
if (user != null && modelAndView != null) {
modelAndView.addObject("loginUser", user);
}
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
hostHolder.clear();
}
}
util包下CookieUtil.java:
public class CookieUtil {
//静态方法,不用容器调用了
public static String getValue(HttpServletRequest request, String name) {
if (request == null || name == null) {
throw new IllegalArgumentException("参数为空!");
}
Cookie[] cookies = request.getCookies();//得到了cookies列表
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals(name)) {//遍历,找对应name的value
return cookie.getValue();
}
}
}
return null;
}
}
userService.java加一个方法,查找凭证
public LoginTicket findLoginTicket(String ticket) {
return loginTicketMapper.selectByTicket(ticket);
}
util包下,HostHolder.java用ThreadLocal保存用户实例user(代替session对象),保证并发时,线程隔离。
@Component
public class HostHolder {
private ThreadLocal<User> users = new ThreadLocal<>();
public void setUsers(User user) {
users.set(user);
}
public User getUsers() {
return users.get();
}
public void clear() {
users.remove();
}
}
2、配置拦截器WebMvcConfig
注入拦截器,配置要排除的路径和要加入的路径
@Configuration//配置类,一般是生成第三方bean,这里实现接口
public class WebMvcConfig implements WebMvcConfigurer {
//注入拦截器,给它配置
@Autowired
private AlphaInterceptor alphaInterceptor;
@Autowired
private LoginTicketInterceptor loginTicketInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(alphaInterceptor)
.excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg")
.addPathPatterns("/register", "/login");
registry.addInterceptor(loginTicketInterceptor)
.excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
}
}
3、改写index.html
header头部中,更改是否显示消息、注册、登录、头像等(动态显示)。
model中有loginUser:显示消息、头像(动态)
model中没有loginUser:显示注册、登录
并发知识补充:为什么用ThreadLocal存user?——保证并发下的线程隔离
ThreadLocal如何保证线程隔离?每个线程对应一个map,这个map里面的键值对是ThreadLocal和value,所以多个线程操作的是各自自己的map,往各自的map里面set、get、remove,实现了线程隔离。
小tip:这里的map是ThreadLocal类里面的静态内部类ThreadLocalMap的对象,它有自己的set、getEntry、remove方法。
ThreadLocal的set、get、remove方法源码如下图:可以看到都是对当前线程操作,然后去当前线程对应的map,然后对map来set、getEntry和remove,底层实际上是用的ThreadLocalMap类的方法: