关于springMVC 参数自动注入的几个问题

springMVC 的威力

现今用java 开发web 服务端在大陆来说是一件比主流还主流的事,而最火热的框架无外乎就是spring 全家桶了。

而在这么一个主流环境中,无论是springboot 还是springcloud 都逃不出采用spring + springMVC + mybatis 的三板斧。

而作为一个服务端开发、一个无情的接口制作机器,如何跟前端传递过来的参数打好交道是重中之重。这里就不得不提今天的主角:超级好用的springMVC 了。

你要问springMVC 好用在哪?Talk is cheap!

假设我们有一张产品表,产品表有主键id,名称name,价格price 这么3个字段。

@Data
public class Product {
	private id;
	private name;
	private price;
}

回忆一下,刀耕火种的时代,我们要保存一份产品的数据,是如何做的呢?

首先是前端传这么一份数据过来(假设前端工程师采用表单提交)。

<form action="addProduct" method="post">
	NAME:<input type="text" name="name"/><br/>
	PRICE:<input type="text" name="price"/><br/>
	<input type="submit" value="submit"/>
</form>

而作为一个远古时期的java 工程师该怎么处理这些数据?假设我们还在写servlet 好了。

public class ProductAddServlet extends HttpServlet {
	protect void service(HttpServletRequest request, HttpServletResponse response)
		throws ServletException, IOException {
		request.setCharacterEncoding("UTF-8");
		Product product = new Product();
		product.setId(IDUtils.getId());
		product.setName(request.getParameter("name"));
		product.setPrice(request.getParameter("price"));
		new ProductDAO().add(product);
	}
}

看起来还不赖?那么让我们把时间拨回到现代,采用springMVC 该怎么接收这些参数呢?

@RestController
public class ProductController {
	@PostMapping("/addProduct")
	public void add(Product product) {
		product.setId(IDUtils.getId());
		productMapper.add(product);
	}
}

为了对比起来更直观,这里跳过service 层直接使用ORM 来处理数据。(实际工作中千万不要这么做哦,spring 里事务是在service 层切入的 !)

是的你没看错,直接丢出来一个product 实体类,所有的参数都由springMVC 帮你自动注入好了。

可能这里product 表只有3个字段,看不出来特别便捷之处。但是当你在实际开发中面对N 张几十个字段的表时,springMVC 的威力就显现出来了。

springMVC 好用归好用,但是本人在工作里,新手上路,还是踩了一些相关的坑。这里记录一下作为前车之鉴。且听我细细道来。

多表重名字段的参数接收

最近在做一个订单系统的时候,订单表涉及到的字段特别多,考虑到性能原因,就根据功能主次拆成A B C D 4张表。

其中A 为主表,B C D为子表,用orderID作为逻辑外键相关联。结果在做update 功能的时候就遇到问题了。接口代码大概长这样:

@PutMapping(value = "/update")
public Object update(A a, B b, C c, D d) {
	if (null != a)
		aService.update(a);
	if (null != b)
		bService.update(b);
	if (null != c)
		cService.update(c);
	if (null != d)
		dService.update(d);
	return ResultBean.setSuccess();
}

正常思路是前端传什么字段过来,后端就更新对应的那张表。问题是A B C D 中有好几个同名字段,比如id name orderID 等等之类的。

那么前端传name 过来,后端springMVC 怎么知道这个name 到底是A B C D中谁的呢?

答案当然是不知道,毕竟连人都分不清谁是谁。springMVC 更是只能一脸懵逼地报错了。

而把参数全列出来,给重名的取别名也不是一个好的解决方案,因为字段实在是太多了,不然就不会拆表啦!

求助于搜索引擎后,得出了这么个解决方法:在原有的4个实体类外面再包一层,用一个汇总的DTO 来接收参数。

@Data
public class OrderDTO {
	private A a;
	private B b;
	private C C;
	private D d;
}

那么在接口文档中就要跟前端约束一下,例如传参name : a.name 或是 b.name 等等。如此这般就能解决重名参数如何辨别的问题了。

再来看看后台接口如何写:

@PutMapping(value = "/update")
public Object update(OrderDTO orderDTO) {
	if (null != orderDTO.getA())
		aService.update(orderDTO.getA());
	if (null != orderDTO.getB())
		bService.update(orderDTO.getB());
	if (null != orderDTO.getC())
		cService.update(orderDTO.getC());
	if (null != orderDTO.getD())
		dService.update(orderDTO.getD());
	return ResultBean.setSuccess();
}

其实换个角度用OOP 的思想来思考, 这是一件很自然而然的事:本来在实际生活里的一个对象,却因为设计被拆成4个部分,那么在业务逻辑里也应该重新封装成一个类去处理。

文件数组的接收

也是在工作中遇到的一个问题,在联调第三方接口的时候,需要传文件,比较变态的是接口文档中要求以文件数组的形似传过去。

好嘛,那我干脆在写接口存本地数据的时候就用文件数组,接口大概长这样:

@PostMapping(value = "/save")
public Object save(Params params, MultipartFile[] files) {
	...
}

前端同事问我这个接口怎么传,想当然的,我告诉他前端应该传一个文件数组过来。以为这样springMVC 便会自动注入到files 这个参数内。

但实际上,联调的时候发现服务端并没有接收到任何文件,debug 时看到files 的长度为0。

我突然意识到,springMVC 除了会自动注入对象属性外,对于数组也按类似的方法进行注入的。

最后我让前端同事把多个文件拆开单个传,所有的文件对应的参数名都叫files ,果然服务端就能正常接收文件了。

日期参数的接收

我们都知道前后端交互,传递的数据都是字符串。对于一个标准的从年月日精确到时分秒的日期格式的字符串,springMVC 可以自动注入到一个Date 类型的参数内,并自动转换类型。

但实际上,前端有时候并不会传一个如此精确的格式过来,比如有的地方只用到了年月日。如果后端不做任何处理,springMVC 会没办法从String 转换成Date ,从而抛出异常。

在网上查了一下,除了后端先接收字符串再在代码里做日期转换之外,还有比较方便的解决方法——在你想要转换的字段属性上使用这个注解:

@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date ctime;

此外,有时候前端要求后台返回的日期需要某种特定的格式的字符串,而不是时间戳,也是可以通过注解解决的:

@JsonFormat(pattern = "yyyy-MM", timezone="GMT+8")
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date ctime;

最后提一下,如果要在代码内做日期格式化处理,SimpleDateFormat因为是线程不安全的,一旦有并发环境,可能会出现深坑,所以应当尽量弃用该类。