问题
开发restful api,大部分时候都要实现根据id获取对象的api,一般来说代码是这样的
class UserController {
@Autowired
UserService userService;
@GetMapping("/{id}")
User getDetail(@PathVariable("id") Long id) {
User user = userService.findById(id);
if (user == null) throw new RuntimeException("not found");
// do something with user
return user;
}
}
这段代码所实现的是根据id获取Entity对象,然后判断Entity对象是否存在,如果不存在则直接抛出异常,避免接下来的操作。
可以看到,只要有根据id获取Entity的地方,就会出现上面这种模式的代码,一个成熟的项目,这些模式少说也要出现十几二十次,代码重复多了,写起来累,而且还容易出bug,比如有的地方没有对Entity做非null校验,就有可能出NPE了。
去除重复代码,提高健壮性
如果Spring能够直接从id获取Entity,并且注入到getDetail的参数中,就可以避免这些重复的代码,就像这样:
class UserController {
@Autowired
UserService userService;
@GetMapping("/{id}")
User getDetail(@PathVariable("id") User user) {
// do something with user
return user;
}
}
并且在注入user的同时,还能判断是否user != null,如果成立直接抛出异常,并且返回404。
初步解决方案
Spring其实已经提供了操作controller参数的方法,如果用的@PathVariable注解controller method的参数,Spring会调用PathVariableMethodArgumentResolver对url中的参数进行转换,转换结果就是controller method的参数。
PathVariableMethodArgumentResolver在转换时,会先根据类型找到对应的converter,然后调用Converter转换。所以可以增加一个自定义的Converter,把id转化为user,如下:
@Component
public class IdToUserConverter implements Converter {
@Autowired
UserMapper userMapper;
@Override
public User convert(String source) {
User user = userMapper.selectByPrimaryKey(source);
if (user == null) throw new RuntimeException("not found");
return user;
}
}
并且还要将自定义的IdToUserConverter注册到Spring的converter库里。
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
@Autowired
IdToUserConverter idToUserConverter;
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(idToUserConverter);
}
}
这下只要在controller这样写,
User getDetail(@PathVariable("id") User user) {...}
就解决了写重复代码和校验user!=null的问题,避免写重复代码。
优化解决方案
不过这样写还有个问题,现实情况下不可能只有User一个Entity,如果每个Entity都要写一个IdToSomeEntityConverter,还是很麻烦。
要解决这个问题,需要一个前提条件,就是必须使用统一的dao层,并且必须给Entity一个统一的类型。我使用的是tk mybatis为mapper提供统一的接口方法。代码如下:
public interface UserMapper extends BaseMapper {}
public interface RoleMapper extends BaseMapper {}
然后定义一个标签接口,所有Entity类都实现这个接口
interface SupportConverter {}
class User implement SupportConverter {...}
class Role implement SupportConverter {...}
BaseMapper提供了selectByPrimaryKey方法,可以根据Entity的Id获取Entity。如果所有的mapper都有这个方法,那就方便进行统一处理了。
除了统一mapper的接口,原来的IdToUserConverter只能处理User一种类型,为了处理多种Entity类型,要把Converter换成ConverterFactory,ConverterFactory可以支持对一个类型的子类型选择对应的converter。ConverterFactory实现如下:
@Component
public class IdToEntityConverterFactory implements ConverterFactory {
private static final Map CONVERTER_MAP=new HashMap<>();
@Autowired
ApplicationContext applicationContext;
@Override
public Converter getConverter(Class targetType) {
if (CONVERTER_MAP.get(targetType) == null) {
CONVERTER_MAP.put(targetType, new IdToEntityConverter(targetType));
}
return CONVERTER_MAP.get(targetType);
}
private class IdToEntityConverter implements Converter {
private final Class tClass;
public IdToEntityConverter(Class tClass) {
this.tClass = tClass;
}
@Override
public T convert(String source) {
String[] beanNames = applicationContext.getBeanNamesForType(ResolvableType.forClassWithGenerics(BaseMapper.class, tClass));
BaseMapper mapper = (BaseMapper) applicationContext.getBean(beanNames[0]);
T result = (T) mapper.selectByPrimaryKey(Long.parseLong(source));
if (result == null) throw new DataNotFoundException(tClass.getSimpleName() + " not found");
return result;
}
}
}
最后,把自定义的IdToEntityConverterFactory注册到Spring的formatter,
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
@Autowired
IdToEntityConverterFactory idToEntityConverterFactory;
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverterFactory(idToEntityConverterFactory);
}
}
现在代码重复的问题解决了,如果后续要增加新的Entity,只要让Entity实现SupportConverter,并且提供继承BaseMapper的mapper,那么就可以自动支持@PathVariable 注解参数转Entity对象了。