简化 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() 就是调用 FactoryBean 的地方,通过 BeanDefinitionRegistryPostProcessor 实现。另外,其中调用 scannerPackages() 是为了得到那些 interface 类,通过 ResourceLoaderAware 实现 。

扫描包的没什么好说的了,常规操作。另外一个小插曲是我想优化逻辑,减少重复,于是把 ApplicationContextAware 用自创的 DiContextUtils 代替,最终发现是不行了,因为 DiContextUtils 的 Context 还没 Aware……

DiContextUtils 源码位于这里,也是执行控制器方法调用 Service 依赖的逻辑。

最后更新:使用方法(23-5-29 Edit)

后来我发现 Spring MVC 原生支持了,压根不需要自己去实现。这里保留下我最终版本的使用方法吧。

在启动 main 函数中加入 Bean:

import com.ajaxjs.framework.spring.BaseWebMvcConfigure;
import com.ajaxjs.framework.spring.EmbeddedTomcatStarter;
import com.ajaxjs.framework.spring.easy_controller.ServiceBeanDefinitionRegistry;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

@Configuration
@EnableWebMvc
@ComponentScan("com.ajaxjs.user")
public class UserApp extends BaseWebMvcConfigure {
    public static void main(String[] args) {
        EmbeddedTomcatStarter.start(8300, UserApp.class, UserConfig.class);
    }
    
    @Bean
    ServiceBeanDefinitionRegistry ServiceBeanDefinitionRegistry() {
        return ServiceBeanDefinitionRegistry.init(getClass());
    }
}

建立 Controller:

import com.ajaxjs.framework.spring.easy_controller.anno.InterfaceBasedController;
import com.ajaxjs.user.service.UserService;
import com.ajaxjs.user.service.UserServiceImpl;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


@RestController
@RequestMapping("/user")
@InterfaceBasedController(serviceClass = UserServiceImpl.class)
public interface UserController extends UserService {
}

Service 接口,不允许有类控制器的注解:

import com.ajaxjs.framework.spring.easy_controller.anno.ControllerMethod;
import com.ajaxjs.user.model.User;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;

public interface UserService {
    @GetMapping("/{id}")
    @ControllerMethod
    User info(@PathVariable Long id);

    @PostMapping
    @ControllerMethod
    Long create(@RequestBody User user);

    /**
     * 检查用户某个值是否已经存在一样的值
     *
     * @param field 字段名,当前只能是 username/email/phone 中的任意一种
     * @param value 字段值,要校验的值
     * @return 是否已经存在一样的值,true 表示存在
     */
    @GetMapping("/checkRepeat")
    @ControllerMethod("检查用户某个值是否已经存在一样的值")
    Boolean checkRepeat(@RequestParam String field, @RequestParam Object value);

    @PutMapping
    @ControllerMethod
    Boolean update(@RequestBody User user);

    @DeleteMapping("/{id}")
    Boolean delete(@PathVariable Long id);
}

源码放在

小结

这种模式非常好用,不但 Spring Boot, Spring MVC 也可以用。原理也在这里说了,希望大家喜欢。若有不足敬请指教。

完整源码在:https://gitee.com/sp42_admin/ajaxjs/tree/master/aj-framework/src/main/java/com/ajaxjs/spring/easy_controller

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sp42a

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值