简化 Spring 控制器:只须写接口即可
Posted sp42a
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了简化 Spring 控制器:只须写接口即可相关的知识,希望对你有一定的参考价值。
大伙有没有觉得 Spring Web 项目中,写了 Controller 又要写 Service,而它们都是很相似的,差不多的方法名字、参数、返回值……为什么不能减少重复写在一起呢?其实 MVC 时代,C 就是 Controller 包办所有,但后来人说这样不好,业务逻辑不应该写在 Controller 里面,应该写在 Service 里面然后让 Controller 去调用,Controller 呢?就负责一些参数校验呀,结果转换返回之类的杂项工作。
一直如此没啥问题。后来技术升级了带来了 Java 注解,很多配置的内容都通过注解完成,不用你去写 Java 调用方法,精简到……Controller 里面只有一行调用 Service 的方法……于是我们考虑能不能把这一行代码都省呢?——答案是肯定的!我们就来看看怎么做。
用法
首先看看我们的控制器现在长什么样子,既然是接口,那应该就是——
@RestController
@InterfaceBasedController(serviceClass = RecognitionProcessService.class)
@RequestMapping("/aip")
public interface RecognitionProcessController
@PostMapping("/recognition_task")
@ControllerMethod("创建图像拼接识别任务")
RecognitionTask createRecognitionTask(@RequestBody RecognitionTask task);
@GetMapping("/list_zip/taskId")
@ControllerMethod("列出压缩包")
ZipResult listZip(@PathVariable String taskId, @RequestParam String task_type, @RequestParam(required = false) String filename);
@PostMapping("/select_reco/taskId")
@ControllerMethod("选择图像拼接识别压缩包")
Boolean selectReco(@PathVariable String taskId, @RequestBody Map<String, Object> params);
@GetMapping("/task/taskId/result")
@ControllerMethod("获取识别进度")
Map<String, Object> getRecognitionResult(@PathVariable String taskId, @RequestParam String task_type);
/** -------三维------ **/
@PostMapping("/model_render_task")
@ControllerMethod("创建三维模型渲染任务")
ModelRenderTask createModelRenderTask(@RequestBody Map<String, Object> params);
主要还是 Spring MVC 那一套配置控制器的注解,一切都没变——最显著不同是 class
声明变成了 interface
声明,而且没有方法实现。——没有方法实现怎么调 Service 方法呢?这里先卖个关子,下文再说。
我们看到,新增的自定义注解有以下有个:
- 接口上有
@InterfaceBasedController
,说明这是一个控制器配置接口,有配置Class<?> serviceClass()
,说明对应的 Service 类 - 方法签名上有
@ControllerMethod
,这个用途有两个:注释说明这方法干嘛的,另外一个(可选的)说明对应的 Service 类和方法。
所以我们晓得,只要 interface 方法签名跟 Service 方法一模一样,那通过 Java 反射去调用不就行了吗? 确实如此——但中间省略了万字解释,——如果你有兴趣,可以接着看,下面会讲讲原理。
顺便看看业务类长什么样子。
@Service
public class RecognitionTaskMgrService implements RecognitionProcessDao
PageResult<RecognitionTask> getRecognitionTaskList(String task_status, String task_no, Integer pageNo, Integer pageSize)
……
……
@InterfaceBasedController
指定好对应的业务就行,那么所有控制器接口方法均一一对应业务类。如果你想单独配置某个方法对应的业务类也行,配置一下 @ControllerMethod
的 serviceClass
和 methodName
参数即可。
这样,与其说把控制器退化为纯粹配置的地方,不如说是一种关注点分离的精彩案例。首先你得了解为什么不能把控制器和业务方法混在一起写,带来的好处是什么,才能理解我们后续如此这般的工作又能改进些什么,——我觉得,至少能减少开发者的“心智”,总是好的,interface 简单清晰,不该有的东西就拿掉(指实现部分),——那不是挺好么?
原理分析
看似很简单的用法,实质背后的原理是什么呢?总的来说,有以下两点:
- Java Proxy 动态代理(最关键的)
- Spring 框架的一些配套用法
请你要记住,但凡有 intreface 却不要求你写实现的,背后的技术大多为“动态代理”,例如 MyBatis,将 SQL 定义通过注解定义在 interface 上,即是一例。
动态代理
动态代理增加了 Java 程序的动态性,减少了强类型本身要求的约束性,比如说方法返回值为 Object,此处就没严格要求跟方法返回值的一致,当然你返回了不一致是会报错的。
动态代理的用法上分为两大步骤,首先是执行方法的逻辑定义,即 InvocationHandler.invoke()
方法重写,其次是代理实例的生成,这个比较简单的说。
InvocationHandler.invoke()
位于 ControllerProxy
类,完整源码如下。
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import org.springframework.util.ReflectionUtils;
import com.ajaxjs.spring.DiContextUtil;
/**
* 通过动态代理执行控制器
*
* @author Frank Cheung<sp42@qq.com>
*
*/
public class ControllerProxy implements InvocationHandler
/**
* 控制器接口类
*/
private Class<?> interfaceType;
/**
* 创建一个 ServiceProxy
*
* @param interfaceType 控制器接口类
*/
public ControllerProxy(Class<?> interfaceType)
this.interfaceType = interfaceType;
/**
* 操作的说明,保存于此
*/
public static ThreadLocal<String> ACTION_COMMNET = new ThreadLocal<>();
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
// System.out.println("调用前,args = " + args);
ControllerMethod annotation = method.getAnnotation(ControllerMethod.class);
if (annotation != null)
Class<?> serviceClass = annotation.serviceClass();
if (serviceClass.equals(Object.class))
// 读取类上的配置
InterfaceBasedController clzAnn = interfaceType.getAnnotation(InterfaceBasedController.class);
serviceClass = clzAnn.serviceClass();
// Object serviceBean = ctx.getBean(serviceClass);
Object serviceBean = DiContextUtil.getBean(serviceClass);
String serviceMethod = annotation.methodName();
if ("".equals(serviceMethod))
serviceMethod = method.getName(); // 如果没设置方法,则与控制器的一致
String comment = annotation.value(); // 处理说明
if (!"".equals(serviceMethod))
ACTION_COMMNET.remove();
ACTION_COMMNET.set(comment);
Method beanMethod = ReflectionUtils.findMethod(serviceBean.getClass(), serviceMethod, method.getParameterTypes());
if (beanMethod == null)
throw new NullPointerException("是否绑定 Service 类错误?找不到" + serviceBean.getClass() + "目标方法");
beanMethod.setAccessible(true);
Object result = ReflectionUtils.invokeMethod(beanMethod, serviceBean, args);
// System.out.println("调用后,result = " + result);
return result;
else
return ReflectionUtils.invokeMethod(method, this, args);
能进入这里执行的都是控制器接口方法,然后查找注解对应的业务方法,非常简单,没有特别晦涩的地方,再有就是 Java 反射的应用,要好好熟悉一下才行。
@InterfaceBasedController
和 @ControllerMethod
两个注解的源码如下。
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 说明这是一个控制器配置接口
*
* @author Frank Cheung<sp42@qq.com>
*
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface InterfaceBasedController
/**
* 对应的业务类
*
* @return
*/
Class<?> serviceClass() default Object.class;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 标记接口控制器里面的控制器方法
*
* @author Frank Cheung<sp42@qq.com>
*
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ControllerMethod
/**
* 注释
*
* @return
*/
String value() default "";
/**
* 对应的业务类
*
* @return
*/
Class<?> serviceClass() default Object.class;
/**
* 对应的方法名称
*
* @return
*/
String methodName() default "";
所谓只写接口不用写实现,实际就是 InvocationHandler.invoke()
里面写了动态处理的语句,而所谓代理实例的生成,当然不是通过 new
生成,而是这样的:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import org.springframework.beans.factory.FactoryBean;
/**
* 接口实例工厂,这里主要是用于提供接口的实例对象
*
* @author Frank Cheung<sp42@qq.com>
*/
public class ControllerFactory implements FactoryBean<Object>
/**
* 控制器接口类
*/
private Class<?> interfaceType;
/**
* 创建一个 ControllerFactory
*
* @param interfaceType 控制器接口类
*/
public ControllerFactory(Class<?> interfaceType)
this.interfaceType = interfaceType;
@Override
public Object getObject() throws Exception
// 这里主要是创建接口对应的实例,便于注入到 spring 容器中
InvocationHandler handler = new ControllerProxy(interfaceType);
return Proxy.newProxyInstance(interfaceType.getClassLoader(), new Class[] interfaceType , handler);
@Override
public Class<?> getObjectType()
return interfaceType;
@Override
public boolean isSingleton()
return true;
Proxy.newProxyInstance()
,Java 动态代理的写法,没什么好说的了。而这里的 Spring FactoryBean 是什么呢?我们下面接着聊。
Spring 框架深入用法
Spring Bean 分两种,一种普通 Bean,另外一种就是我们这里所说的 FactoryBean,它是由 FactoryBean 工厂实现的。FactoryBean 之目的在于提供一个场所创建复杂逻辑或复杂配置的对象实例。
FactoryBean 需要指定 Bean 类型(于泛型参数中设置)和以下几个重写方法。
public Object getObject()
创建 Bean 实例public Class<T> getObjectType()
返回 Bean 类型public boolean isSingleton()
是否创建单例
上述的 ControllerFactory
即演示了 FactoryBean,我们控制器没有特别类型,于是设置泛型类型为 Object
。
FactoryBean 怎么被调用呢?有点奇怪,——是放在 Bean 注册器(BeanDefinitionRegistry)中的。这个 BeanDefinitionRegistry 也有点复杂,竟然同一个类实现了三个接口:BeanDefinitionRegistryPostProcessor, ResourceLoaderAware, ApplicationContextAware
。我们先看看完整代码。
import java.io.IOException;
import java.util.LinkedHashSet;
import java.util.Set;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.env.Environment;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternUtils;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.util.ClassUtils;
import com.ajaxjs.util.logger.LogHelper;
/**
* 用于 Spring 动态代理注入自定义接口
*
* @author Frank Cheung<sp42@qq.com>
*
*/
public class ServiceBeanDefinitionRegistry implements BeanDefinitionRegistryPostProcessor, ResourceLoaderAware, ApplicationContextAware
private static final LogHelper LOGGER = LogHelper.getLog(ServiceBeanDefinitionRegistry.class);
/**
* 控制器所在的包
*/
private String controllerPackage;
/**
* 创建一个 ServiceBeanDefinitionRegistry
*
* @param controllerPackage 控制器所在的包
*/
public ServiceBeanDefinitionRegistry(String controllerPackage)
this.controllerPackage = controllerPackage;
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException
LOGGER.info("扫描控制器……");
Set<Class<?>> scannerPackages = scannerPackages(controllerPackage);
// 通过反射获取需要代理的接口的 clazz 列表
for (Class<?> beanClazz : scannerPackages)
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(beanClazz);
GenericBeanDefinition def = (GenericBeanDefinition) builder.getRawBeanDefinition();
/*
* 这里可以给该对象的属性注入对应的实例。mybatis 就在这里注入了 dataSource 和 sqlSessionFactory,
* definition.getPropertyValues().add("interfaceType", beanClazz),BeanClass 需要提供
* setter definition.getConstructorArgumentValues(),BeanClass
* 需要提供包含该属性的构造方法,否则会注入失败
*/
def.getConstructorArgumentValues().addGenericArgumentValue(beanClazz);
// def.getConstructorArgumentValues().addGenericArgumentValue(applicationContext);
/*
* 注意,这里的 BeanClass 是生成 Bean 实例的工厂,不是 Bean 本身。 FactoryBean 是一种特殊的
* Bean,其返回的对象不是指定类的一个实例,其返回的是该工厂 Bean 的 getObject 方法所返回的对象。
*/
def.setBeanClass(ControllerFactory.class);
def.setAutowireMode(GenericBeanDefinition.AUTOWIRE_BY_TYPE);// 这里采用的是 byType 方式注入,类似的还有 byName 等
// String simpleName = beanClazz.getSimpleName();
// LOGGER.info("beanClazz.getSimpleName(): 0", simpleName);
// LOGGER.info("GenericBeanDefinition: 0", definition);
registry.registerBeanDefinition(beanClazz.getSimpleName(), def);
private static final String DEFAULT_RESOURCE_PATTERN = "**/*.class";
private MetadataReaderFactory metadataReaderFactory;
/**
* 根据包路径获取包及子包下的所有类
*
* @param basePackage basePackage
* @return Set<Class<?>>
*/
private Set<Class<?>> scannerPackages(String basePackage)
Set<Class<?>> set = new LinkedHashSet<>();
String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + resolveBasePackage(basePackage) + '/' + DEFAULT_RESOURCE_PATTERN;
try
Resource[] resources = resourcePatternResolver.getResources(packageSearchPath);
for (Resource resource : resources)
if (resource.isReadable())
String clzName = metadataReaderFactory.getMetadataReader(resource).getClassMetadata().getClassName();
Class<?> clazz = Class.forName(clzName);
if (clazz.isInterface() && clazz.getAnnotation(InterfaceBasedController.class) != null)
set.add(clazz);
catch (ClassNotFoundException | IOException e)
LOGGER.warning(e);
return set;
protected String resolveBasePackage(String basePackage)
Environment env = applicationContext.getEnvironment();
String holders = env.resolveRequiredPlaceholders(basePackage);
return ClassUtils.convertClassNameToResourcePath(holders);
private ResourcePatternResolver resourcePatternResolver;
@Override
public void setResourceLoader(ResourceLoader loader)
resourcePatternResolver = ResourcePatternUtils.getResourcePatternResolver(loader);
metadataReaderFactory = new CachingMetadataReaderFactory(loader);
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext cxt) throws BeansException
this.applicationContext = cxt;
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException
postProcessBeanDefinitionRegistry()以上是关于简化 Spring 控制器:只须写接口即可的主要内容,如果未能解决你的问题,请参考以下文章
.net Core在过滤器中获取 系统接口方法(以IMemoryCache 为例) 及HttpContext 获取系统接口