問題
開發(fā)restful api,大部分時(shí)候都要實(shí)現(xiàn)根據(jù)id獲取對(duì)象的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;
}
}
這段代碼所實(shí)現(xiàn)的是根據(jù)id獲取Entity對(duì)象,然后判斷Entity對(duì)象是否存在哟玷,如果不存在則直接拋出異常希停,避免接下來的操作。
可以看到淳衙,只要有根據(jù)id獲取Entity的地方蘑秽,就會(huì)出現(xiàn)上面這種模式的代碼,一個(gè)成熟的項(xiàng)目箫攀,這些模式少說也要出現(xiàn)十幾二十次肠牲,代碼重復(fù)多了,寫起來累靴跛,而且還容易出bug缀雳,比如有的地方?jīng)]有對(duì)Entity做非null校驗(yàn),就有可能出NPE了梢睛。
去除重復(fù)代碼肥印,提高健壯性
如果Spring能夠直接從id獲取Entity识椰,并且注入到getDetail的參數(shù)中,就可以避免這些重復(fù)的代碼深碱,就像這樣:
class UserController {
@Autowired
UserService userService;
@GetMapping("/{id}")
User getDetail(@PathVariable("id") User user) {
// do something with user
return user;
}
}
并且在注入user的同時(shí)腹鹉,還能判斷是否user != null莹痢,如果成立直接拋出異常种蘸,并且返回404。
初步解決方案
Spring其實(shí)已經(jīng)提供了操作controller參數(shù)的方法竞膳,如果用的@PathVariable注解controller method的參數(shù)航瞭,Spring會(huì)調(diào)用PathVariableMethodArgumentResolver對(duì)url中的參數(shù)進(jìn)行轉(zhuǎn)換,轉(zhuǎn)換結(jié)果就是controller method的參數(shù)坦辟。
PathVariableMethodArgumentResolver在轉(zhuǎn)換時(shí)刊侯,會(huì)先根據(jù)類型找到對(duì)應(yīng)的converter,然后調(diào)用Converter轉(zhuǎn)換锉走。所以可以增加一個(gè)自定義的Converter滨彻,把id轉(zhuǎn)化為user,如下:
@Component
public class IdToUserConverter implements Converter<String, User> {
@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) {...}
就解決了寫重復(fù)代碼和校驗(yàn)user!=null的問題梁厉,避免寫重復(fù)代碼辜羊。
優(yōu)化解決方案
不過這樣寫還有個(gè)問題,現(xiàn)實(shí)情況下不可能只有User一個(gè)Entity词顾,如果每個(gè)Entity都要寫一個(gè)IdToSomeEntityConverter八秃,還是很麻煩。
要解決這個(gè)問題肉盹,需要一個(gè)前提條件昔驱,就是必須使用統(tǒng)一的dao層,并且必須給Entity一個(gè)統(tǒng)一的類型上忍。我使用的是tk mybatis為mapper提供統(tǒng)一的接口方法骤肛。代碼如下:
public interface UserMapper extends BaseMapper<User> {}
public interface RoleMapper extends BaseMapper<Role> {}
然后定義一個(gè)標(biāo)簽接口,所有Entity類都實(shí)現(xiàn)這個(gè)接口
interface SupportConverter {}
class User implement SupportConverter {...}
class Role implement SupportConverter {...}
BaseMapper提供了selectByPrimaryKey方法窍蓝,可以根據(jù)Entity的Id獲取Entity萌衬。如果所有的mapper都有這個(gè)方法,那就方便進(jìn)行統(tǒng)一處理了它抱。
除了統(tǒng)一mapper的接口秕豫,原來的IdToUserConverter只能處理User一種類型,為了處理多種Entity類型,要把Converter換成ConverterFactory混移,ConverterFactory可以支持對(duì)一個(gè)類型的子類型選擇對(duì)應(yīng)的converter祠墅。ConverterFactory實(shí)現(xiàn)如下:
@Component
public class IdToEntityConverterFactory implements ConverterFactory<String, SupportConverter> {
private static final Map<Class,Converter> CONVERTER_MAP=new HashMap<>();
@Autowired
ApplicationContext applicationContext;
@Override
public <T extends SupportConverter> Converter<String, T> getConverter(Class<T> targetType) {
if (CONVERTER_MAP.get(targetType) == null) {
CONVERTER_MAP.put(targetType, new IdToEntityConverter(targetType));
}
return CONVERTER_MAP.get(targetType);
}
private class IdToEntityConverter<T extends Audit> implements Converter<String, T> {
private final Class<T> tClass;
public IdToEntityConverter(Class<T> 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);
}
}
現(xiàn)在代碼重復(fù)的問題解決了毁嗦,如果后續(xù)要增加新的Entity,只要讓Entity實(shí)現(xiàn)SupportConverter回铛,并且提供繼承BaseMapper的mapper狗准,那么就可以自動(dòng)支持@PathVariable 注解參數(shù)轉(zhuǎn)Entity對(duì)象了。