Spring Security | Note-3

Spring Security Note-3


服务异常处理

Spring Boot 中默认的错误处理机制

例如:

访问不存在的URL时,返回一个空白的错误页(状态码404);

访问页面http://127.0.0.1:8080/xxx

当浏览器访问时,返回的是一个HTML页面;(Accept: text/html)

当客户端(Restlet Client模拟)访问时,返回的是一个JSON;(Accept: */*)

这是Spring Boot默认的错误返回机制;

public class BasicErrorController extends AbstractErrorController {…}

根据请求头(header)中的信息不同,进行不同的处理;

1
2
3
4
@RequestMapping(produces = {"text/html"})
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
...
}
1
2
3
4
5
@RequestMapping
@ResponseBody
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
...
}

在默认的错误返回机制下,已经可以解决大部分的错误和异常;

自定义异常处理

我们在main/resources文件夹目录下,新建一个目录结构 main/resources/resources/error

在error文件夹目录下,新建一个404.html的页面;

此时,浏览器再次访问http://127.0.0.1:8080/xxx页面,404错误时,不再是返回空白的错误页面;

而是返回自定义的404.html页面的内容;

对于客户端的错误信息如何自定义?

1
2
3
4
5
@GetMapping("/{id:\\d+}")
@JsonView(User.UserDetailView.class)
public User getInfo(@PathVariable(name = "id",required = false) String id){
throw new UserNotExistException(id);
}

通过自定义异常类(extends RuntimeException)

1
2
3
4
5
6
7
8
9
10
11
12
13
public class UserNotExistException extends RuntimeException {
private String id;
public UserNotExistException(String id) {
super("user not exist");
this.id = id;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}

并且在Controller中,定义一个新的类ControllerExceptionHandler,用于处理其他Controller所抛出的所有异常信息;

1
2
3
4
5
6
7
8
9
10
11
12
@ControllerAdvice
public class ControllerExceptionHandler {
@ExceptionHandler(UserNotExistException.class)
@ResponseBody
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Map<String,Object> handleUserNotExistException(UserNotExistException ex){
Map<String ,Object> result = new HashMap<>();
result.put("id",ex.getId());
result.put("message",ex.getMessage());
return result;
}
}

SpringBoot默认异常返回机制

1
2
3
4
5
6
7
8
{
"timestamp": 1535356164785,
"status": 500,
"error": "Internal Server Error",
"exception": "java.lang.RuntimeException",
"message": "user not exist",
"path": "/user/1"
}

自定义异常返回机制

1
2
3
4
{
"id": "1",
"message": "user not exist"
}

使用Filter和Interceptor拦截REST服务

需求:记录所有服务的处理时间

过滤器 Filter

无法获得具体调用的参数,具体的Controller,具体的方法;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Component
public class TimeFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("Time Filter Init");
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("Time Filter Start");
long start = new Date().getTime();
chain.doFilter(request, response);
System.out.println("Time Filter Time Used : " + (new Date().getTime() - start));
System.out.println("Time Filter End");
}

@Override
public void destroy() {
System.out.println("Time Filter Destroy");
}
}

Q:若第三方过滤器的框架,无法进行Spring @Component的注解怎么办?

A:在传统的项目中,有一个web.xml的配置文件,将第三方的过滤器,配置到web.xml文件即可;

A:但是Spring项目不存在web.xml文件,如何配置进去?通过自定义WebConfig的方式;

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class WebConfig {
@Bean
public FilterRegistrationBean timeFilter(){
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
TimeFilter timeFilter = new TimeFilter();
registrationBean.setFilter(timeFilter);
List<String> urls = new ArrayList<>();
urls.add("/*");
registrationBean.setUrlPatterns(urls);
return registrationBean;
}
}
拦截器 Interceptor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Component
public class TimeInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle");
// 获取Controller名字
System.out.println(((HandlerMethod) handler).getBean().getClass().getName());
// 获取方法名字
System.out.println(((HandlerMethod) handler).getMethod().getName());
request.setAttribute("startTime", new Date().getTime());
return true;
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object o, ModelAndView modelAndView) throws Exception {
System.out.println("postHandle");
Long start = (Long) request.getAttribute("startTime");
System.out.println("Time Interceptor Time Used : " + (new Date().getTime() - start));
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("afterCompletion");
Long start = (Long) request.getAttribute("startTime");
System.out.println("Time Interceptor Time Used : " + (new Date().getTime() - start));
System.out.println("ex is : " + ex);
}
}
1
2
3
4
5
6
7
8
9
10
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter{
@Autowired
private TimeInterceptor timeInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(timeInterceptor);
}
}

使用切片拦截REST服务

切片(AOP) Aspect

AOP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Aspect
@Component
public class TimeAspect {
// Around 包含了其余三个@Before @After @AfterThrowing
// execution执行 拦截的类 所有的方法 任何的返回值 任何的参数
@Around("execution(* com.imooc.web.controller.UserController.*(..))")
public Object handleControllerMethod(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("Time Aspect Start");
Object[] args = pjp.getArgs();
for (Object arg : args) {
System.out.println("arg is : " + arg);
}
long start = new Date().getTime();
Object object = pjp.proceed();
System.out.println("Time Aspect Time Used : " + (new Date().getTime() - start));
System.out.println("Time Aspect End");
return object;
}
}

过滤器:可以拿到原始的HTTP请求和相应的信息,但是拿不到处理请求信息方法的信息;

拦截器:即可以拿到原始的HTTP请求和相应的信息,也能拿到处理请求的方法的信息,但是拿不到被调用的参数的值;

切片:可以拿到处理请求的方法的信息,以及真正调用方法的参数的值,但是拿不到原始的HTTp请求和相应的对象;

三个对象各有各特点,根据具体的业务需要,选择具体的拦截机制;

拦截机制的拦截顺序:Filter -> Interceptor -> ControllerAdvice -> Aspect -> Controller


使用REST方式处理文件服务

上传
1
2
3
4
5
6
7
8
9
// 模拟上传到本地
@Test
public void whenUploadSuccess() throws Exception {
String result = mockMvc.perform(fileUpload("/file")
.file(new MockMultipartFile("file","test.txt","multipart/form-data","hello upload".getBytes("UTF-8"))))
.andExpect(MockMvcResultMatchers.status().isOk())
.andReturn().getResponse().getContentAsString();
System.out.println(result);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RestController
@RequestMapping("/file")
public class FileController {
@PostMapping
public FileInfo update(MultipartFile file) throws IOException {
System.out.println(file.getName());
System.out.println(file.getOriginalFilename());
System.out.println(file.getSize());

String folder = "xxx";
File localFile = new File(folder,new Date().getTime() + ".txt");
file.transferTo(localFile);
return new FileInfo(localFile.getAbsolutePath());
}
}
下载
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 通过API下载
@GetMapping("/{id}")
public void download(@PathVariable String id, HttpServletRequest request, HttpServletResponse response) {
try (
InputStream inputStream = new FileInputStream(new File(folder, id + ".txt"));
OutputStream outputStream = response.getOutputStream();
){
response.setContentType("application/x-download");
response.addHeader("Content-Disposition","attachment;filename=test.txt");
IOUtils.copy(inputStream,outputStream);
outputStream.flush();
}catch(Exception e){
}
}