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 {
    @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换成ConverterFactoryConverterFactory可以支持对一个类型的子类型选择对应的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对象了。

你可能感兴趣的:(Spring Boot路由id转化为控制器Entity参数)