Spring实战-读书笔记(七)-Spring MVC的高级技术

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/fly_zxy/article/details/78819572

 Spring实战-读书笔记(七)-Spring MVC的高级技术

Spring MVC的文件上传

我们在从浏览器中选择的文件上传到服务时,请求报文的请求头(Request Headers)中的Content-type为:
Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryr2pw9Slr664W64KT
DispatcherServlet在接受到这样的请求后,会将multipart请求委托给MultipartResolver接口来处理,通过MultipartResolver接口的实现类来处理文件上传,从Spring3.1开始,Spring的MultipartResolver提供了两种实现:
  • CommonsMultipartResolver:使用Jakarta Commons FileUpload解析multipart请求;
  • StandardServletMultipartResolver:依赖于Servlet 3.0对multipart请求的支持(始于Spring 3.1)。

配置文件上传

我们使用CommonsMultipartResolver类来配置文件上传。步骤如下:
步骤一:
在form标签添加enctype属性,表示请求的内容为multipart/form-data。
	<sf:form method="POST" commandName="aboutBlog" enctype="multipart/form-data">
添加文件上传input标签
	<input name="fileResource" type="file"></input>		

步骤二:
配置CommonsMultipartResolver bean,这里是使用java类方式配置bean。
	@Bean
	public MultipartResolver multipartResolver() throws IOException {

		// 使用Spring内置的 CommonsMultipartResolver
		CommonsMultipartResolver cmr = new CommonsMultipartResolver();
		//设置解析请求的编码
		cmr.setDefaultEncoding("UTF-8");
		//设置单个文件上传最大大小,单位:字节
		cmr.setMaxUploadSize(Long.parseLong(env.getProperty("fileUpload.maxSize")));
		//设置上传文件在磁盘的缓存目录
		cmr.setUploadTempDir(new FileSystemResource(env.getProperty("fileUpload.pathTmp")));
		//设置上传文件在内存中的缓存大小,0表示不再内存中缓存上传文件,上传的文件将使用磁盘缓存
		cmr.setMaxInMemorySize(0);
		cmr.setResolveLazily(true);
		return cmr;
	}
MultipartResolver实现类处理上传文件的流程大概是这样的:以及二进制流的形式读取上传文件- >读取的二进制数据流写满缓冲区被 ->写入磁盘缓存目录 ->
if:如果超出设定的文件上传最大值就抛出MaxUploadSizeExceededException异常,注意这个时候还没有到你的Controller方法中。
if:如果没有超出文件上传最大值,调用你的Controller方法处理请求。注意这个时候文件已经写入你的磁盘缓存目录上。

步骤三:
在处理文件上传的方法上添加@RequestPart("fileResource") MultipartFile fileResource参数。
	@RequestMapping(value = "/wirte", method = POST)
	public String wirte(@RequestPart("fileResource") MultipartFile fileResource,@Valid @ModelAttribute("aboutBlog") AboutBlogModel aboutBlogModel,Errors  errors,Model model) throws IllegalStateException, IOException {

		if(errors.hasErrors()) {
			return "showWirteForm";
		}
		String path = env.getProperty("fileUpload.path")+fileResource.getOriginalFilename();		
		fileResource.transferTo(new File(path));
		String id = aboutBlogReq.add(aboutBlogModel);
	
		//这里使用重定向,防止用户重复提交(比如多次点击刷新按钮),注意:这里指定的是controller路径
		return "redirect:/blogManager/queryOne/" + id;
	}
通过MultipartFile我们可以获取上传文件的名称(getOriginalFilename),上传文件大小(getSize)等有用的信息,如果上传的文件是多个使用 @RequestPart("fileResource") MultipartFile [] fileResource参数即可。经过这三部就可以使用Spirng实现文件上传功能。但是只能上传文件是不严谨的,web应用中还需要效验文件的扩展名、是否上传文件和上传文件最大大小,下面我们来看一下如何效验这些项。

校验文件上传

通过Spring提供的对异常处理机制和拦截器机制,我们几乎可以完美的实现对上传文件扩展名、是否上传文件和上传文件最大大小的效验功能。
我们之前说过上传的文件大小超出了我们的设定的上传文件的大小设置将会抛出MaxUploadSizeExceededException异常,我们可以使用这样机制来判断上传文件最大大小校验功能。实现核心代码如下:
package blog.web.execption;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;

import javax.servlet.http.HttpServletRequest;

import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.multipart.MaxUploadSizeExceededException;

import blog.model.AboutBlogModel;

@ControllerAdvice
public class ControllerExceptionHandler {
	
	/**
	 * MaxUploadSizeExceededException异常处理
	 * @param exception
	 * @param request
	 * @return
	 * @throws Exception
	 */
	@ExceptionHandler(MaxUploadSizeExceededException.class)
	public String handlerSizeLimitExceededException(MaxUploadSizeExceededException exception,HttpServletRequest request) throws Exception {
		request.setAttribute("aboutBlog", new AboutBlogModel());
		//String name = request.getParameter("name");
		String message = "上传的文件大小过大,请上传("+(exception.getMaxUploadSize()/1024/1024)+"M)以下的文件";
		return "redirect:/blogManager/wirte?fileResourceMessage="+URLEncoder.encode(message, "UTF-8");
		//return "showWirteForm";
	}
}

使用拦截器机制效验文件扩展名和是否上传文件校验。思路是这样的:首先创建一个拦截器FileUploadSizeValidateInterceptor,拦截器拦截处理文件上传的Controller方法,在方法上使用@ValidateFileNotUpload和@ValidateFileType注解标记,然后再拦截器中的preHandle()方法做效验。代码清单如下:
@ValidateFileNotUpload
package common.annotion;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

/**
 * 用于标记在接受文件处理请求的方法上<br>
 * 表示验证文件是否上传
 * @author Administrator
 *
 */
@Retention(RUNTIME)
@Target(METHOD)
public @interface ValidateFileNotUpload {

	/**
	 * 提示消息
	 * @return
	 */
	String message();
	/**
	 * 重定向地址
	 * @return
	 */
	String sendRedirectUrl() default "";
}

@ValidateFileType
package common.annotion;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

/**
 * 用于标记在接受文件处理请求的方法上<br>
 * 表示验证文件上传类型
 * @author Administrator
 *
 */
@Retention(RUNTIME)
@Target(METHOD)
public @interface ValidateFileType {
	/**
	 * 提示消息
	 * @return
	 */
	String message();
	/**
	 * 重定向地址
	 * @return
	 */
	String sendRedirectUrl() default "";
	/**
	 * 指定上传文件类型类型
	 * @return
	 */
	String [] typeArray() default {};
}

FileUploadSizeValidateInterceptor
package blog.interceptor;

import java.io.FileNotFoundException;
import java.net.URLEncoder;
import java.util.Arrays;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.web.method.HandlerMethod;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import common.annotion.ValidateFileNotUpload;
import common.annotion.ValidateFileType;

public class FileUploadSizeValidateInterceptor extends HandlerInterceptorAdapter {

	private long maxSize = 0;
	private String [] defaultTypes = {};

	public FileUploadSizeValidateInterceptor(long maxSize, String defaultTypes) {
		this.maxSize = maxSize;
		this.defaultTypes = defaultTypes.split(",");
	}

	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
		if (handler.getClass().isAssignableFrom(HandlerMethod.class)) {

			ValidateFileNotUpload validateFileNotUploadAnn = ((HandlerMethod) handler)
					.getMethodAnnotation(ValidateFileNotUpload.class);
			ValidateFileType validateFileTypeAnn = ((HandlerMethod) handler)
					.getMethodAnnotation(ValidateFileType.class);
			
			//验证是否上传了文件
			if(validateFileNotUploadAnn != null) {
				String message = validateFileNotUploadAnn.message();
				String sendRedirectUrl = validateFileNotUploadAnn.sendRedirectUrl();
				if("".equals(sendRedirectUrl)) {
					sendRedirectUrl = request.getServletPath();
				}
				boolean validateEmpty = validateFileNotUpload(request, response, handler);
				if (!validateEmpty) {
					response.sendRedirect(request.getContextPath() + sendRedirectUrl + "?fileResourceMessage="
							+ URLEncoder.encode(message, "UTF-8"));
					return false;
				}
			}
			
			//验证上传文件是否为指定类型
			if(validateFileTypeAnn!=null) {		
				String message = validateFileTypeAnn.message();
				String sendRedirectUrl = validateFileTypeAnn.sendRedirectUrl();
				String [] typeArray = validateFileTypeAnn.typeArray();
				if("".equals(sendRedirectUrl)) {
					sendRedirectUrl = request.getServletPath();
				}
				if(typeArray.length==0) {
					 typeArray=defaultTypes;
				}
				boolean validateType = validateType(request, response, handler,typeArray);
				if (!validateType) {
					response.sendRedirect(request.getContextPath() + sendRedirectUrl + "?fileResourceMessage="
							+ URLEncoder.encode(message, "UTF-8"));
					return false;
				}
			}
		}
		return true;
	}

	/**
	 * 验证文件类型
	 * 
	 * @param request
	 * @param response
	 * @param handler
	 * @param typeArray 
	 * @return
	 * @throws Exception
	 */
	private boolean validateType(HttpServletRequest request, HttpServletResponse response, Object handler, String[] typeArray)
			throws Exception {
		if (request instanceof MultipartHttpServletRequest) {
			MultipartHttpServletRequest multipartReqeust = (MultipartHttpServletRequest) request;
			Map<String, MultipartFile> fileMap = multipartReqeust.getFileMap();
			for (String key : fileMap.keySet()) {
				MultipartFile file = fileMap.get(key);
				int index = file.getOriginalFilename().indexOf(".");
				String fileType = file.getOriginalFilename().substring(index + 1, file.getOriginalFilename().length());
				if (!Arrays.asList(typeArray).contains(fileType)) {
					return false;
				}
			}
		}
		return true;
	}

	/**
	 * 验证是否上传了文件
	 * 
	 * @param request
	 * @param response
	 * @param handler
	 * @return
	 */
	private boolean validateFileNotUpload(HttpServletRequest request, HttpServletResponse response, Object handler) {
		if (request instanceof MultipartHttpServletRequest) {
			MultipartHttpServletRequest multipartReqeust = (MultipartHttpServletRequest) request;
			Map<String, MultipartFile> fileMap = multipartReqeust.getFileMap();
			for(String key:fileMap.keySet()) {
				MultipartFile multipartFile = fileMap.get(key);
				if(multipartFile.isEmpty()) {
					return false;
				}
			}
		}
		return true;
	}
}

注册拦截器
	/**
	 * 注册拦截器
	 */
	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		FileUploadSizeValidateInterceptor fileUploadSizeValidateInterceptor = 
				new FileUploadSizeValidateInterceptor(Long.parseLong(env.getProperty("fileUpload.maxSize")),env.getProperty("fileUpload.defaultTypes"));
		registry.addInterceptor(fileUploadSizeValidateInterceptor).addPathPatterns("/blogManager/wirte");
	}

Controller方法
@ValidateFileNotUpload(message = "请上传文件.")
	@ValidateFileType(message = "请上传正确的文件类型.")
	@RequestMapping(value = "/wirte", method = POST)
	public String wirte(@RequestPart("fileResource") MultipartFile fileResource,@Valid @ModelAttribute("aboutBlog") AboutBlogModel aboutBlogModel,Errors  errors,Model model) throws IllegalStateException, IOException {

		if(errors.hasErrors()) {
			return "showWirteForm";
		}
		String path = env.getProperty("fileUpload.path")+fileResource.getOriginalFilename();
		fileResource.transferTo(new File(path));
		String id = aboutBlogReq.add(aboutBlogModel);
	
		//这里使用重定向,防止用户重复提交(比如多次点击刷新按钮),注意:这里指定的是controller路径
		return "redirect:/blogManager/queryOne/" + id;
	}

异常处理

我们使用Spring MVC构建的web应用中,不可避免的会出错。但是当某个地方出错我们该怎么处理呢?如果一个请求出了错,我们该怎么给客户端响应呢?spring提供了三种方式将异常转换为响应:
  • 特定的Spring异常映射为http状态码;
  • 将自定义的异常类映射为http状态码,使用@ResponseStatus注解实现。
  • 在方法上添加@ExceptionHandler注解,使用方法中的逻辑来处理异常。

特定的Spring异常映射为http状态码

Spring会自带一些异常,这些异常在抛出的时候回转换成http状态码。一下是转换关系:
BindException 400 - Bad Request
ConversionNotSupportedException 500 - Internal Server Error
HttpMediaTypeNotAcceptableException 406 - Not Acceptable
HttpMediaTypeNotSupportedException 415 - Unsupported Media Type
HttpMessageNotReadableException 400 - Bad Request
HttpMessageNotWritableException 500 - Internal Server Error
HttpRequestMethodNotSupportedException 405 - Method Not Allowed
MethodArgumentNotValidException 400 - Bad Request
MissingServletRequestParameterException 400 - Bad Request
MissingServletRequestPartException 400 - Bad Request
NoSuchRequestHandlingMethodException 404 - Not Found
TypeMismatchException 400 - Bad

比如我们请求一个请求,DispatcherServlet没有找到请求地址对应的Controller方法,Disp在处理时将会抛出NoSuchRequestHandlingMethodException异常,最总给客户端响应的是404状态码。没有找到请求资源呗!!!

将自定义异常类映射为http状态码

使用@ResponseStatus注解可以将异常类映射为http状态码。例如我们有一个showUserInfo()Controller方法处理根据ID查询用户信息的请求,如果queryUserInfo(long id)方法没有查询到用户信息,那么就抛出UserInfoNotFoundException异常,返回给浏览器的响应是404状态码。代码实现如下:
UserInfoNotFoundException
@ResponseStatus(value=HttpStatus.NOT_FOUND)
public class UserInfoNotFoundException extends RuntimeException {

}
Controller方法
	@RequestMapping(value="/showUserInfo/{id}",method=POST)
	public String showUserInfo(@PathVariable() long id,Model model) {
			UserInfo userInfo = userInfoSer.queryUser(id);
			if(userInfo == null) {
				throw new UserInfoNotFoundException();
			}
			model.addAttribute("userInfo", userInfo);
			return "redirect:/showUserInfo";
	}
Controller方法UserInfoNotFoundException异常抛出异常后页面将会显示404http状态码错误。

使用方法中的逻辑来处理异常和@ExceptionHandler注解

在文件上传时验证文件是否超出了设置的最大大小时已经使用过了这种定义异常的方式。假设我们有多个控制器中的都有处理文件上传请求的方法,每个方法都可能因为上传的文件过大抛出MaxUploadSizeExceededException异常,我们可以在每个Controller中定义下面代码所展示的方法,来处理每一个controller方法所抛出的MaxUploadSizeExceededException异常,但是这就重复造轮子了,同样的代码出现在了很多个controler中。在Spring 3.2对这样的问题引入了一个解决方案:控制器通知(controller advice)。其实现原理其实就是AOP,在处理请求的过程中某个切点上应用通知而已。在类上使用了@ControllerAdvice注解标记的类就控制器通知,其中使用@ExceptionHandler注解标记的方法作用是处理Controller方法在处理请求过程中抛出的特定异常。

在每个Controller中定义的异常处理方法
	/**
	 * MaxUploadSizeExceededException异常处理
	 * @param exception
	 * @param request
	 * @return
	 * @throws Exception
	 */
	@ExceptionHandler(MaxUploadSizeExceededException.class)
	public String handlerSizeLimitExceededException(MaxUploadSizeExceededException exception,HttpServletRequest request) throws Exception {
		request.setAttribute("aboutBlog", new AboutBlogModel());
		//String name = request.getParameter("name");
		String message = "上传的文件大小过大,请上传("+(exception.getMaxUploadSize()/1024/1024)+"M)以下的文件";
		return "redirect:/blogManager/wirte?fileResourceMessage="+URLEncoder.encode(message, "UTF-8");
		//return "showWirteForm";
	}

控制器通知类。handlerSizeLimitExceededException方法是对MaxUploadSizeExceededException异常的处理逻辑。这样我们就不用在多个controller中包含handlerSizeLimitExceededException()。
package blog.web.execption;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;

import javax.servlet.http.HttpServletRequest;

import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.multipart.MaxUploadSizeExceededException;

import blog.model.AboutBlogModel;

@ControllerAdvice
public class ControllerExceptionHandler {
	
	/**
	 * MaxUploadSizeExceededException异常处理
	 * @param exception
	 * @param request
	 * @return
	 * @throws Exception
	 */
	@ExceptionHandler(MaxUploadSizeExceededException.class)
	public String handlerSizeLimitExceededException(MaxUploadSizeExceededException exception,HttpServletRequest request) throws Exception {
		request.setAttribute("aboutBlog", new AboutBlogModel());
		//String name = request.getParameter("name");
		String message = "上传的文件大小过大,请上传("+(exception.getMaxUploadSize()/1024/1024)+"M)以下的文件";
		return "redirect:/blogManager/wirte?fileResourceMessage="+URLEncoder.encode(message, "UTF-8");
		//return "showWirteForm";
	}
}

重定向请求传递数据

controller方法中分会前缀为"redirect:"表示重定向,重定向到目标方法之后会丢失request、response对象,也就是说会丢失Mode(模型对象),我么可以利用以下两种方式来实现重定向传递数据:
  1. 使用URL路径参数或查询参数。这种方式只能传递简单的参数,并不能传递一个复杂的对象。
  2. 通过flash属性发送数据。此方法可传递一个复杂的对象。
事例代码
	@RequestMapping(value = "/wirte", method = POST)
	public String wirte(Classify classify, RedirectAttributes model) {
		String id = classifyRepository.add(classify);
		// 将属性信息添加到RedirectAttributes中,使重定向到目标页面后,目标页面依然可以获取到classify信息
		model.addFlashAttribute("classify", classify);
		return "redirect:/classifyManager/classifyInfo/" + id;
	}

	@RequestMapping(value = "/classifyInfo/{id}", method = GET)
	public String classifyInfo(@PathVariable("id") String id, Model model) {
		// 如果存在classify信息返回直接视图
		// 如果不存在classify信息,则根据id查询classify信息并添加到模型中
		if (!model.containsAttribute("classify")) {
			model.addAttribute("classify", classifyRepository.queryOne(id));
		}
		return "queryOneClassifyInfo";
	}
RedirectAttributtes是Model的一个子类,处理有Model功能外,还提供了几个用于设置flash属性的方法。例如示例代码中的addFlashAttribute()用于设置一个flash属性和值。重定向请求发送数据的原理是:在重定向之前将faslh属性放到session中,在目标方法中取出flash属性。



猜你喜欢

转载自blog.csdn.net/fly_zxy/article/details/78819572
今日推荐