美文网首页
Spring Boot路由id转化为控制器Entity参数

Spring Boot路由id转化为控制器Entity参数

作者: x_ae1f | 来源:发表于2018-04-26 14:04 被阅读0次

    问题

    开发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<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) {...}
    

    就解决了写重复代码和校验user!=null的问题,避免写重复代码。

    优化解决方案

    不过这样写还有个问题,现实情况下不可能只有User一个Entity,如果每个Entity都要写一个IdToSomeEntityConverter,还是很麻烦。
    要解决这个问题,需要一个前提条件,就是必须使用统一的dao层,并且必须给Entity一个统一的类型。我使用的是tk mybatis为mapper提供统一的接口方法。代码如下:

    public interface UserMapper extends BaseMapper<User> {}
    
    public interface RoleMapper extends BaseMapper<Role> {}
    

    然后定义一个标签接口,所有Entity类都实现这个接口

    interface SupportConverter {}
    
    class User implement SupportConverter {...} 
    
    class Role implement SupportConverter {...}
    

    BaseMapper提供了selectByPrimaryKey方法,可以根据Entity的Id获取Entity。如果所有的mapper都有这个方法,那就方便进行统一处理了。

    除了统一mapper的接口,原来的IdToUserConverter只能处理User一种类型,为了处理多种Entity类型,要把Converter换成ConverterFactoryConverterFactory可以支持对一个类型的子类型选择对应的converter。ConverterFactory实现如下:

    @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);
        }
    }
    

    现在代码重复的问题解决了,如果后续要增加新的Entity,只要让Entity实现SupportConverter,并且提供继承BaseMapper的mapper,那么就可以自动支持@PathVariable 注解参数转Entity对象了。

    相关文章

      网友评论

          本文标题:Spring Boot路由id转化为控制器Entity参数

          本文链接:https://www.haomeiwen.com/subject/bigplftx.html