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

问题

开发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 Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,881评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,941评论 6 342
  • 要加“m”说明是MB,否则就是KB了. -Xms:初始值 -Xmx:最大值 -Xmn:最小值 java -Xms8...
    dadong0505阅读 4,905评论 0 53
  • 有时候会觉得自己仿佛被世界抛弃,孤独感在自己内心无限的扩大,最终沉溺在黑暗的角落里,听着悲哀的歌曲、看着漫无边际的...
    KaisenH阅读 257评论 4 3
  • 我凝望夜空中闪烁的群星 从没有一颗向我眨眼 我谛听寂静中细碎的声响 每一声都让我想起家乡 我回忆童年时美好的画面 ...
    东京森林阅读 253评论 0 2