简化 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 指定好对应的业务就行,那么所有控制器接口方法均一一对应业务类。如果你想单独配置某个方法对应的业务类也行,配置一下 @ControllerMethodserviceClassmethodName 参数即可。

这样,与其说把控制器退化为纯粹配置的地方,不如说是一种关注点分离的精彩案例。首先你得了解为什么不能把控制器和业务方法混在一起写,带来的好处是什么,才能理解我们后续如此这般的工作又能改进些什么,——我觉得,至少能减少开发者的“心智”,总是好的,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 控制器:只须写接口即可的主要内容,如果未能解决你的问题,请参考以下文章

简化 Spring 控制器:只须写接口即可

.net Core在过滤器中获取 系统接口方法(以IMemoryCache 为例) 及HttpContext 获取系统接口

Java之十五 JDBC编程

IOC容器

SpringBoot2.0基础案例(01):环境搭建和RestFul风格接口

SpringBoot2.0基础案例(01):环境搭建和RestFul风格接口