在 Spring Boot 中使用拦截器(“拦截器” 是 AOP 的思想的体现),可在以下情况下执行操作 :
简单逻辑的拦截器:
// 拦截器
@Component
public class UserAccessInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("pre hander method is calling ");
// 返回 true 才能继续进入 Controller
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
System.out.println("post hander method is calling");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
System.out.println("afterCompletion method is calling");
}
}
注册拦截器:
@Configuration
// 注册拦截器
public class InterceptorConfig implements WebMvcConfigurer {
@Autowired
private UserAccessInterceptor userAccessInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(userAccessInterceptor)
// 拦截的路径
.addPathPatterns("/addPerson")
// 不拦截的路径
.excludePathPatterns("/user/loggin");
}
}
(这里说不拦截的路径应该是指排除访问路径,尤其是静态页面。之前 Spring Boot 2.x 依赖的 spring WebMVC 5.x版本,相对于 Spring Boot 1.5.x 依赖的 spring WebMVC 4.3.x 版本而言,针对资源的拦截器初始化时有区别,前者会拦截静态资源,所以需要对仍要访问的静态资源通过 excludePathPatterns 方法 进行排除。) (但是我发现现在这个版本,是不会对静态资源进行拦截的,是否就不需要排除了呢?)
然后在 之前 PersonController 中添加一行代码来验证 postHandle() 方法的执行:
@GetMapping("/addPerson")
public ResponseResult addPerson(@RequestBody Person person){
int ret= personService.addPerson(person);
// 在将 响应 发送给 客户端 之前,即 在 controller return 之前。
System.out.println("addPerson Method is calling.");
return ret == 1 ? ResponseResult.success(null) : ResponseResult.error("新增用户失败");
}
}
运行结果:
如果修改拦截路径为不存在的:.addPathPatterns("/addPersons")
,再去访问 /addPerson,运行结果:
可以看到,那三个方法并不会执行,连 pre… 都不执行,相当于拦截器失效啦。 (那可不是嘛,你访问的路径没有被拦截,拦截器可不就不起作用了。)
过滤器是用于拦截应用程序的 HTTP 请求 和 响应 的对象,拦截的是 servlet 。
// 过滤器
@WebServlet
public class MyFilter implements Filter {
@Override
// 在系统启动时就会执行
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("init Method start...");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
// doFilter 方法之前的代码在 Serlet 执行之前先执行
System.out.println("doFilter Method start...");
filterChain.doFilter(servletRequest, servletResponse);
// doFilter 方法之后的代码在 Serlet 执行之前先执行
System.out.println("doFilter Method end...");
}
@Override
public void destroy() {
System.out.println("destory Method start...");
}
}
提供一个 Servlet:
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("Get Method start--------------");
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("Post Method start--------------");
resp.getWriter().print("return post method start
");
}
}
为 Servlet 配置路径,在 App.java 中添加方法:
//@Bean
public ServletRegistrationBean myServlet() {
return new ServletRegistrationBean(new MyServlet(), "/dofilter");
}
@Order(2)
public class MyFilter1 implements Filter {...
运行结果:(不是请求一次就终止了,可以多次请求的,效果像是循环。)
例子:过滤非法字符,先获取参数 name 的值,如果是 “赌博”,就进行过滤,再不往下执行,在 过滤器1 中修改 doFilter 方法即可:
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
// doFilter 方法之前的代码在 Serlet 执行之前先执行
System.out.println("doFilter1 Method start...");
// 获取 name 值
String name = servletRequest.getParameter("name");
System.out.println("name=====" + name);
if (name != null && name.equals("赌博")) {
// 直接返回
return;
}
filterChain.doFilter(servletRequest, servletResponse);
// doFilter 方法之后的代码在 Serlet 执行之前先执行
System.out.println("doFilter1 Method end...");
}
依次传入参数 name=嘉、赌博、名字,在传入 “赌博” 时,访问页面是空白的,控制台输出如下:
可以看到,传入“赌博”时,过滤器1 doFilter 没有执行,也就是说,这时并没有进到 Servlet 中,以及 doFilter 之后的方法也没再执行了,所以这里没有打印 “doFilter1 Method end…” ,而 另一个过滤器都执行着呢。
接下来给刚开始的过滤器(也就是外层那个过滤器)中添加过滤 “赌博” 的逻辑,过滤器1 只是打印字符串,第一次就传入参数 name=赌博,页面自然是空白的,运行结果:
可以看到,在最外层就 return 了,不会进入过滤器链。
再有一个问题,后来运行程序时,我发现这两个过滤器对所有的 Servlet 都进行了过滤,于是我用 @WebFilter(urlPatterns = {"/dofilter"})
注解指定过滤路径,但是并不起作用,这里的问题就是,同时使用 @WebFilter 和 @Component,Spring Boot将会自动注册过滤器,不管写成什么拦截地址,过滤器注册的地址都是 “/*”。 (真的好坑 !)
解决方法:
使用注册 Bean 的方式,在 自己写的过滤类 Myfilter 上使用 @Component 注解,让 Spring 容器能识别到过滤器组件,然后 通过 FilterRegistrationBean 对象的 addUrlPatterns 方法来指定过滤器的过滤地址:
@Configuration
public class Config {
@Autowired
Myfilter myfilter;
@Bean
public FilterRegistrationBean registrationProjectFilter() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(myfilter);
registration.addUrlPatterns("/dofilter");
return registration;
}
}
像 jdbctemplate、redistemplate 、RestTemple 都是设计模式的一种体现。
一个应用,可能有 APP 端、PC 端、小程序端,这些 前端(有给用户展示的页面的 笼统地成为 “前端”) 调用微服务的接口就是 RESTFUL 接口,走的都是 HTTP 通信。
RestTemple 是 Spring 提供的 用于访问 Http 请求的客户端,RestTemple 提供了多种简洁的远程访问服务的方法,省去了很多无用的代码。
相信大家之前都用过 apache 的 HTTPClient 类,逻辑繁琐,代码复杂,还要自己编写使用类 HttpClientUtil,封装对应的 post,get,delete 等方法。
✨ RestTemplate 的行为可以通过 callback 回调方法 和 配置HttpMessageConverter 来定制,用来把对象封装到 HTTP 请求体,将响应信息放到一个对象中。RestTemplate 提供更高等级的符合 HTTP 的 六种主要方法,可以很简单的调用 RESTful 服务。 ✨
Rest 模板用于创建 使用 RESTful Web 服务的应用程序。它的底层其实就是 HttpClient,封装成了模板。
在 App.java 中创建 RestTemplate 对象:
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
默认情况下 RestTemplate 帮我们自动注册了一组 HttpMessageConverter 用来处理一些不同的 contentType 的请求。如果 现有的转换器 不能满足你的需求,还可以实现 org.springframework.http.converter.HttpMessageConverter 接口自己写转换器。详情参考官方api https://docs.spring.io/spring-framework/docs/4.3.7.RELEASE/javadoc-api/org/springframework/http/converter/package-summary.html
对于一个请求来说,超期时间,请求连接时间等都是必不可少的参数,为了更好的适应业务需求,可以自己修改 restTemplate 的配置。
通过使用 RestTemplate 的 postForObject() 或者 exchange() / execute() 方法来使用 GET API。
先给项目打个包,端口号是 8080,提供个方法,对于 name = jia1、jia2 或者 jia3 的 person 对象会加到 ret 列表中,返回值是 ret 列表:
static ArrayList<Person> list = new ArrayList<>();
static {
list.add(new Person("1", "jia1"));
list.add(new Person("2", "jia2"));
list.add(new Person("3", "jia3"));
}
@RequestMapping(value = "/user1", method = RequestMethod.GET)
public ArrayList<Person> queryUser(@RequestParam(name = "name", required = false) String name) {
if(name == null){
return list;}
ArrayList<Person> ret=new ArrayList<>();
for(Person person:list){
if(name.equals(person.getName())){
ret.add(person);
}
}
return ret;
}
稍后用 jar 包形式启动。
再在 IDEA (端口号是 9999)里调用接口:
@RequestMapping(value = "/user1", method = RequestMethod.GET)
public List<Person> queryUser(@RequestParam(name = "name", required = false) String name) {
// {}表示占位符,1 表示一个参数
List list=restTemplate.getForObject("http://localhost:8080/user1?name={1}",list.class,name);
return list;
}
使用 postman 访问:
可以看到,9999 端口里的方法调用了 8080 端口的方法。这种方式模拟的就是 在微服务中,不同的服务部署在不同的虚拟机上,一个服务需要访问另一台机器上的服务的场景。
@RequestMapping(value = "/user2", method = RequestMethod.GET)
public List<Person> queryUser1(@RequestParam(name = "name", required = false) String name) {
// 方法二
HashMap<String, Object> map = new HashMap<>();
map.put("name", name);
List list = restTemplate.getForObject("http://localhost:8080/user1?name={1}", List.class, map);
return list;
}
运行结果:
@RequestMapping(value = "/user2", method = RequestMethod.GET)
public List<Person> queryUser1(@RequestParam(name = "name", required = false) String name) {
// 方法三
HttpHeaders headers=new HttpHeaders();
// 接受 Json 数据
headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
HttpEntity httpEntity = new HttpEntity(headers);
List Retlist = restTemplate.exchange("http://localhost:8080/user1?name={1}", HttpMethod.GET,httpEntity,List.class,name)
.getBody();
return Retlist;
}
// 方法四
HttpHeaders headers=new HttpHeaders();
// 接受 Json 数据
headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
HttpEntity httpEntity = new HttpEntity(headers);
HashMap<String,Object> map = new HashMap<>();
map.put("name",name);
List Retlist = restTemplate.exchange("http://localhost:8080/user1?name={name}", HttpMethod.GET,httpEntity,List.class,map)
.getBody();
return Retlist;
演示第一种方法 getForObject() 方法 的:
@PostMapping(value = "/user1")
public ArrayList<Person> createUser(@RequestBody Person person) {
list.add(person);
return list;
}
// POST 方法
@RequestMapping(value = "/user2", method = RequestMethod.POST)
public List<Person> createUser1(@RequestBody Person person) {
list = (ArrayList<Person>) restTemplate.postForObject("http://localhost:8080/user1", person, List.class);
return list;
}
// 方法二
HttpHeaders headers = new HttpHeaders();
// 接受 Json 数据
headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
HttpEntity<Object> httpEntity = new HttpEntity(person,headers);
return restTemplate.exchange("http://localhost:8080/user1",HttpMethod.POST,httpEntity,List.class)
.getBody();
演示第一种方法 getForObject() 方法 的,是没有返回值的:
// put 方法
@PutMapping(value = "/user1/{id}")
public ArrayList<Person> updateUser(@PathVariable("id") String id, @RequestBody Person person) {
System.out.println("id from user ====" + id);
System.out.println("person====" + person);
for (Person person1 : list) {
if (id.equals(person1.getId())) {
list.set(list.indexOf(person1), person);
}
}
return list;
}
@PutMapping(value = "/user2/{id}")
public void updateUser1(@PathVariable("id") String id, @RequestBody Person person) {
// 方法一 没有返回值
restTemplate.put("http://localhost:8080/user1/{1}", person, id);
}
运行结果:
可以看到,因为 “/user2/{id}” 路径对应的方法返回类型为 void,所以响应为空白,但是在 /user1 端 可以看到是更新成功了的:
演示第二种方法 exchage() 方法 的,是有返回值的 :
// 方法二
@PutMapping(value = "/user2/{id}")
public List<Person> updateUser1(@PathVariable("id") String id, @RequestBody Person person) {
HttpHeaders headers = new HttpHeaders();
// 接受 Json 数据
headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
HttpEntity<Object> httpEntity = new HttpEntity(person, headers);
return restTemplate.exchange("http://localhost:8080/user1/{1}", HttpMethod.PUT, httpEntity, List.class, id)
.getBody();
注意,删除不能只是遍历数组,然后调用 remove(Object o) 方法,比如,写成这样子:
@DeleteMapping(value = "/user1/{id}")
public ArrayList<Person> deleteUser(@PathVariable("id") String id) {
for (Person person1 : list) {
if (id != null && person1.getId().equals(id)) {
list.remove(person1);
}
}
System.out.println("List" + list);
return list;
}
因为 remove 里涉及 size,如果 remove 了,size 就会减一,那如果有重复的 id,按照 id 去查询,就会出现漏删的情况;而且每次 remove 前都需要调用 next 方法的,详情见这篇博客 https://blog.csdn.net/weixin_41750142/article/details/109518530源码如下:
应该写成这样:
@DeleteMapping(value = "/user1/{id}")
public ArrayList<Person> deleteUser(@PathVariable("id") String id) {
for (int i=0;i<list.size();i++) {
if (id != null && list.get(i).getId().equals(id)) {
list.remove(i);
i--;
}
}
System.out.println("List" + list);
return list;
}
比如说,第一次删除了下标为 1 的元素,这时候 i–,再经历 i++,i = 1 ,开始新的循环,这时遍历到下标为 1 的元素,这样的情况是合理的,因为刚刚执行了 remove,原先下标为 2 的元素就会往前挪一位的,要是直接 i++,就跳过了往前挪的这个元素。
演示第一种方法 getForObject() 方法 的,是没有返回值的:
@DeleteMapping(value = "/user2/{id}")
public void deleteUser1(@PathVariable("id") String id) {
restTemplate.delete("http://localhost:8080/user1/{1}", id);
}
运行结果:
因为返回值是 void,所以显示空白,来看 /user1 端 :
演示第二种方法 exchage() 方法 的,是有返回值的 :
@DeleteMapping(value = "/user2/{id}")
public List<Person> deleteUser1(@PathVariable("id") String id) {
HttpHeaders headers = new HttpHeaders();
// 接受 Json 数据
headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
HttpEntity<Object> httpEntity = new HttpEntity(headers);
return restTemplate.exchange("http://localhost:8080/user1/{1}", HttpMethod.DELETE, httpEntity, List.class, id)
.getBody();
}
(其实这几个方法熟悉一个了就会其他的了,照猫画虎… 照葫芦画瓢…)
@RestController
public class FileController {
@RequestMapping(value = "/upload", method = RequestMethod.POST, consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public String fileUpload(@RequestParam("file") MultipartFile file) throws IOException {
File file1 = new File("d:/" + file.getOriginalFilename());
file1.createNewFile();
FileOutputStream fileOutputStream = new FileOutputStream(file1);
fileOutputStream.write(file.getBytes());
fileOutputStream.close();
return "file is upload success.";
}
}
@RequestMapping(value = "/download", method = RequestMethod.GET)
// 这里演示把文件在页面上展示出来,不是真正的下载
public ResponseEntity<Object> downFile(HttpServletResponse response) throws IOException {
String fileName = "d:/Ready to study.txt";
File file = new File(fileName);
FileSystemResource fileSystemResource = new FileSystemResource(file);
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Disposition", "attachment; " +
"filename=" + file.getName());
headers.add("Cache-Control", "no-cache, no-store, must-revalidate");
headers.add("Pragma", "no-cache");
headers.add("Expires", "0");
return ResponseEntity.ok().
headers(headers).
contentLength(fileSystemResource.contentLength()).
contentType(MediaType.parseMediaType("application/octet-stream")).
body(new InputStreamResource(fileSystemResource.getInputStream()));
}
(1)拦截器需要实现 HandlerInterceptor 接口,使用 @Component 注解,根据需要 实现 preHandle方法—— 在 Controller 之前(返回 true 才能进入 Controller)、postHandle 方法——在 Controller return返回前、afterCompletion 方法 前端请求完毕后,然后使用 @Configuration 注解 注册拦截器,配置拦截和不拦截的路径。
(2)过滤器需要实现 Filter 接口,使用 @WebServlet 注解,doFilter 方法之前的代码在 Serlet 执行之前先执行,doFilter 方法之后的代码在 Serlet 执行之后先执行。
(3)RestTemplate 适用于 在微服务中,不同的服务部署在不同的虚拟机上,一个服务需要访问另一台机器上的服务的场景 。比如本文中的例子本地 9090 调用 8080 的 方法,当然,调用的方法肯定是要相同的,使用相应的 GET、POST、PUT、DELETE API 即可,主要是 getForObject 方法(注意传入 HashMap 类型参数时,需要注意泛型参数需要是
二者都是 AOP 编程思想的体现,都能实现权限检查、日志记录等。
拦截器是 Spring 框架中通过反射实现的,是 Spring 的组件,因此能使用 Spring 中的任何资源、对象,比如 Service 对象 通过 IOC 注入到拦截器即可,而过滤器不能;而 过滤器是 Servlet 的规范。
拦截器拦的是 Controller 层,而 过滤器拦截的是 Servlet,实际上 Controller 是在 Servlet 之后,也就是说,过滤器比拦截器早执行。
过滤器只在 Servlet 前后起作用,而拦截器能够深入到方法前后、异常抛出前后等,在 Spring 框架的程序中,优先考虑用拦截器。