Spring MVC注解版本--初识--12- 初识基于注解的Controller
- 基于注解的Controller原型分析
- 自定义用于基于注解的Controller的HandlerMapping
- 官方提供用于处理注解的HandlerMapping
- 自定义用于基于注解的Controller的HandlerAdaptor
初识基于注解的Controller
在Spring MVC框架中,传统的Handler类型,比如Controller或者ThrowawayController,都需 要具体实现类继承某个基类或者实现某个接口。而使用基于注解的Controller的话,则没有这样的限制。实际上,基于注解的controller就是一个普通的POJO,只是使用某些注解附加了一些相关的元数据信息而已。下面是某个基于注解的Controller实现:
代码语言:javascript复制@Controller
public class HelloController {
@RequestMapping("/hello")
public Stu sayHello(@RequestParam("name") String name, @RequestParam("age") Integer age){
return new Stu(name,age);
}
}
可以看到,我们的controller实现类HelloController 无拘无束,不需要实现任 何强制接口类型,或者去继承哪个父类。最主要的是也不需要去依赖Servlet API,甚至Spring MVC相关的API。HelloController 只作为一个普通的POJO而存在。不过,应用程序中类似 的POJO到处可见,如果不能通过某种方式加以区分,Spring MVC显然无法知道,到底哪个POJO才是用于Web请求处理的Controller实现类。纵使要“大海捞针”,我们也得知道“针”到底是个什么样子不是?
传统的Controller或者ThrowawayController通过接口类型作为标志的方式,基于注解的Controller则采用标注于具体实现类上的某种类型的注解作为标志。在HelloController类的定义中,我们使用@Controller和@RequestMapping两种类型的注解来标注该类,以告知Spring MVC框架HelloController 可以作为处理某一Web请求的Controller实现类。
原则上来说, 只需要在webApplicationContext中添加如下配置:
代码语言:javascript复制<context:component-scan base-package="your.controller.package"/>
然后将各种基于注解的controller实现类定义在your.controller.package下即可。剩下的事情,像如何获取并调用这些基于注解的Controller等,就由Spring MVC框架帮我们全部揽下来了。
对于/hello形式的请求,Spring MVC将使用HelloController作为处理请求的Handler,并调用其标注了@RequestMapping的sayHello方法进行当前 Web请求的处理。
怎么样?
较之传统的Handler类型,使用基于注解的Controller是不是看起来要简洁得多?
基本上,只要在指定的包下面定义用于处理Web请求的Handler对象(任何你我喜欢的对象类型),然后使用指定的注解类型标注它们就行,完全省却了在Java文件与XML配置文件之间切换的烦恼。
最主要的是,这些基于注解的Controller甚至不依赖于Servlet API。而且,你或许还能找出更多喜爱它的理由。不得不承认,对于日常开发来说,事情确实简单了不少。但是,难道你就不想一探基于注解的Controller之下到底隐藏着什么样的秘密吗?
如果没有Spring MVC框架幕后的某种支持,你想啊,孤伶伶的一个使用某种注解类型标注的POJO,又能够发挥什么样的作用呢?
基于注解的Controller原型分析
在我们完全明白Spring MVC框架的整个结构之后,添加个基于注解的Controller已经不再是什么高难度动作了。对于Spring MVC框架来说,基于注解的Controller和传统的Controller或者 ThrowawayController在本质上并没什么区别,它们全都是框架内用于处理Web请求的Handler。如果 我们可以达成这样的共识,那么理解基于注解的Controller的运作机制就并非难事了。
基于注解的controller是Spring 2.5之后才出现的Handler形式。我们不妨先让自己回到“史前文明”,看一下要在Spring 2.5之前版本的Spring MVC中使用基于注解的Controller需要做哪些工作,问题实际上就简化为,如何实现自定义的Handler类型,对吗?
显然,基于注解的Controller这种类型的Handler定义我们已经有了,只是一些添加了某种类型注解标注的POJO而已,下面要做的是要搞清楚如下两个问题。
- 如何让Spring MVC框架类(其实就是DispatcherServlet)知道当前Web请求应该由哪个基于注解的Controller处理?
- 如何让Spring MVC框架类知道调用基于注解的Controller的哪个方法来处理具体的Web请求?
用我们的行话来说就是,我们需要为基于注解的Controller提供相应的HandlerMapping以处理Web请求到Handler的映射关系,需要提供相应的HandlerAdaptor以调用并执行自定义handler的 处理逻辑。
自定义用于基于注解的Controller的HandlerMapping
在基于注解的Controller中,当前实现类将用于哪个Web请求的处理是由相应的注解标注的。
比如,@RequestMapping(“/hello”)标注的HelloController将 用于URL为"helloController"的Web请求处理。这些注解中的信息,我们需要通过Java 的反射机制(Java Reflection)来读取。
无论是现有的BeanNameUrlHandlerMapping还是SimpleUrlHandlerMapping,显然都没有提供通过反射读取注解中映射信息的功能。
所以,我们不得不为基于注解的Controller提供特定的HandlerMapping实现。
要实现一个专门用于基于注解的Controller的HandlerMapping实现,从原理上来说并不困难。我们所要作的只是遍历所有可用的基于注解的Controller实现类,然后根据请求的路径信息,与实现类中的注解所标注的请求处理映射信息进行比对。
如果当前基于注解的Controller实现类的注解所包含的信息与请求的路径信息相匹配,那么就返回当前这一基于注解的Controller实现类即可。
根据注解新提供Web请求映射功能的HandlerMapping原型代码示例:
代码语言:javascript复制public class AnnotationBaseHandlerMapping implements HandlerMapping {
@Override
public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
HandlerExecutionChain chain=null;
Object[] annoControllers=getAvailableAnnotationControllers();
for (Object annoController : annoControllers) {
Class<?> annoClass = annoController.getClass();
if(annoClass.isAnnotationPresent(RequestMapping.class)){
RequestMapping mapping = annoClass.getAnnotation(RequestMapping.class);
if(match(mapping,request)){
chain=new HandlerExecutionChain(annoController);
if(!CollectionUtils.isEmpty(getHandlerInterceptors())){
chain.addInterceptors(getHandlerInterceptors().toArray());
break;
}
}
};
}
return chain;
}
}
在HandlerMapping接口必须实现的getHandler(…)方法中,我们首先遍历了所有可用的基于注解的Controller,然后通过反射获取@RequestMapping的相应信息与请求信息进行匹配,并最终返回匹配后的结果。这一总体逻辑很好理解,我们可能存在疑问的地方是后面的两个辅助方法,即getAvailableAnnotaitionControllers () 和matches (…)
getAvailableAnnotaitionControllers ()方法用于获取所有基于注解的controller实现类。如果你愿意,当然可以把所有的这些标注了注解的Controller实现类添加到webApplicationContext,然后注入给我们的AnnotationBasedHandlerMapping,并在getAvailableAnnotationControllers ()方法中直接返回。
不过,使用基于注解的Controller的一个优势就在于,不需要在webApplicationcontext中添加任何配置。所以,即使能这么做,我们也不应如此。不妨将这个问题暂且,搁下,稍后我们再来揭示Spring 2.5中是如何处理它的,目前我们只要了解这个方法是做什么的就可以。
至于matches(…)方法,其实现逻辑完全就是一个获取@RequestMapping所包含的信息,然后与当前请求进行对比的过程,这可以借助于某些框架类进行匹配,比如org.springframework.util.PathMatcher和org.springframework.web.util.UrlPathHelper等。当然,如果你不辞辛苦,完全凭一己之力去写所有匹配代码来完成这一工作也是可以的。
现在,只要将我们的AnnotationBasedHandlerMapping添加到DispatcherServlet的字距WebApplicationContext,理论上它就能正确处理Web请求到具体基于注解的Controller之间的映射关系。但原型终归是原型,它只是帮助我们简化问题的难度,从而了解一件事情本质上的东西。要应用于实际环境还有许多路要走,好在,这段路已经有人走过了。
官方提供用于处理注解的HandlerMapping
实际上,正如我们所预料的那样,Spring 2.5中基于注解的Controller确实依赖于某一个官方的HandlerMapping实现来处理映射关系,那就是DefaultAnnotationHandlerMapping。
DefaultAnnotationHandlerMapping在实现原理上与 我们的AnnotationBasedHandlerMapping原型相似,它会首先扫描应用程序的Classpath,通过反射 获取所有标注了@Controller的对象,之后就可以像AnnotationBasedHandlerMapping所展示的 那样来完成一个HandlerMapping的职责了。
至于扫描Classpath然后获取标注了@Controller的对象 这样的工作,是由<context:component-scan/>
来完成的。这也就是为什么需要我们的基于注解的Controller实现类必须标注@Controller类型的这一注解的原因。
因为<context:component- scan/>
内置了对@Controller的支持,但并不一定内置了其他你所需要的注解类型的支持。如果你不想使用@Controller,那么就得自己实现扫描classpath并获取相应controller对象的逻辑。
不过 那又何苦呢?当然,不管你最终如何选择,我想,看到这里,如何实现原型中的getAvailableAnnotationControllers()方法你已经心中有数了吧?
注意
DefaultAnnotationHandlerMapping实现逻辑要比AnnotationBasedHandlerMapping原型复杂得多。随着后面对基于注解的Controller特性的深入,我们就会认识到这一点。
在2.5版本的Spring MVC中,DefaultAnnotationHandlerMapping将在DispatcherServlet初始化的时候就被默认启用(随同一起的还有BeanNameUr1HandlerMapping)。除非想要定制它的某些行为,大多情况下,并不需要在DispatcherServlet的webApplicationContext中明确地声明它。
在Spring 5. 版本中,DispathcerServlet默认启用的HandlerMapping有如下几个:
自定义用于基于注解的Controller的HandlerAdaptor
有了针对于基于注解的Controller的HandlerMapping实现,只是完成了一半的工作,要让基于注解的Controller真正工作起来,我们还需要完成剩下的那一半,即实现针对基于注解的Controller的自定义HandlerAdaptor。
HandlerMapping返回了处理Web请求的某个基于注解的controller实例,而DispatcherServlet并不知道应该去调用该实例的哪个方法来处理Web请求。为了能够以统一的方式调用各种类型的Handler,DispatcherServlet需要一个针对基于注解的Controller的HandlerAdaptor实现。 这跟“传统的Controller需要一个SimpleControllerHandlerAdapter,ThrowawayController需 要一个ThrowawayControllerHandlerAdapter”是一个道理。
为了构建一个针对基于注解的Controller的HandlerAdaptor原型实现,我们得回头看一下基于注解的Controller所定义的Web请求处理方法有何特征。我们知道,对于传统的Controller来说,SimpleControllerHandlerAdapter只需要调用其接口中所定义的handleRequest(…)方法即 可。对于ThrowawayController情况也是相似的,ThrowawayControllerHandlerAdapter知道 execute()方法就是ThrowawayController所定义的Web请求处理方法,而基于注解的Controller 显然无法找到这样的契约关系。不过,我们依然可以找到特定于它的Web请求处理方法,那就是使用@RequestMapping标注的方法定义。(@RequestMapping可以有两种用途,我们稍后详述。)
与DefaultAnnotationHandlerMapping(或者我们的AnnotationBasedHandlerMapping)能够 使用反射获取相应注解的信息这一做法类似,针对基于注解的Controller的HandlerAdaptor原型实现,同样可以如法炮制,只要通过反射查找标注了@RequestMapping的方法定义,然后通过反射调用该方法,并返回DispatcherServlet所需要的ModelAndView即可。有了这样的思路,如下所示的一个HandlerAdaptor原型也就不难构建了。
代码语言:javascript复制public class AnnotationControllerHandlerAdaptor implements HandlerAdapter {
@Override
public boolean supports(Object handler) {
Class<?> clazz = handler.getClass();
return clazz.isAnnotationPresent(Controller.class) ||
clazz.isAnnotationPresent(RequestMapping.class);
}
@Override
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Method[] methods = handler.getClass().getDeclaredMethods();
for (Method method : methods) {
if(method.isAnnotationPresent(RequestMapping.class)){
ModelAndView mav = invokeAndReturn(method, handler, request);
return mav;
}
}
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return null;
}
private ModelAndView invokeAndReturn(Method method, Object handler, HttpServletRequest request) {
//1.使用DataBinder或者其他装备将request参数绑定到方法参数
Object[] parameterValues=bind(request,method);
//2.使用绑定后获得的相应参数调用方法
Object ret=method.invoke(handler,parameterValues);
ModelAndView mav = new ModelAndView();
if(ret instanceof String)
mav.setViewName((String) ret);
else if(ret instanceof ModelMap)
mav.addAllObjects((Map<String, ?>) ret);
else
...;
return mav;
}
@Override
public long getLastModified(HttpServletRequest request, Object handler) {
return -1;
}
}
实际上,一个真正用于生产环境下,对应基于注解的Controller的HandlerAdaptor实现类要考虑的事情很多。而我们的原型实现因为要将问题简化,所以并没涉及,如下所述。
如何根据@RequestMapping提供的各种信息来决定是否调用当前方法?
- 我们的原型只要发现当前方法标注有@RequestMapping,即认为它就是将被调用的Web请求处理方法,那如果 @RequestMapping指定了metethod={ReequestMethod.POST),我们同样可能在Web请求以GET形式发送的时候调用当前方法,这显然是违反@RequestMapping语义的。
如何在数据绑定期间决定将哪个请求参数绑定到方法的哪个参数上?
- 通过现有的反射API可以获取当前方法的参数类型,但无法获取方法参数的名称,单靠反射API显然无法识别请求参数到方法参数一对一的映射关系,也就无法实现正确的数据绑定。为了解决这个问题,Spring 2.5在实现类似的功能的时候,使用了ObjectWeb的ASM类库帮助解决方法参数名称的获取问题。
- 方法参数同样可能持有相应的注解,显然需要为这些标注有相应注解的方法参数的处理提供更多的分支逻辑,以囊括所有可能的情况,比如,如果指定Web请求参数中的author需要绑定到处理方法的name方法参数上去,我们可能需要定义处理方法如下:
public String processMethod(@RequestParam("author")String name,...){
...
}
这种情况下,我们不能依赖默认的请求参数与方法参数名称匹配的原则进行数据绑定,而应该以@RequestParam所指定的绑定原则为准。这就需要我们添加更多的代码逻辑支持。
- 如果基于注解的Controller中某些数据需要通过HttpSession进行管理,而基于注解的Controller又不依赖于任何ServletAPI,该如何在HandlerAdaptor中提供代码逻辑支持呢?
- 基于注解的Controller的请求处理方法返回值有没有限制?如果有,可以定义哪些类型?如果没有,HandlerAdaptor要如何枚举所有可能的返回值类型呢?我们的原型中只是考虑了两种类型。如果有其他的类型,难道要继续枚举未知的类型?在Spring 2.5的基于注解的Controller中,处理方法的返回值类型只能有规定的几种,所以,这个问题选择了比较简单的解决方案。
- 如果基于注解的Controller需要访问模型数据或者返回某些模型数据,我们的HandlerAdaptor可以通过哪些方式提供支持?是让具体的处理方法通过返回值将模型参数返回,然后由HandlerAdaptor添加到要返回给DispatcherServlet使用的ModelAndView中,还是由 HandlerAdaptor实现为基于注解的Controller传递一个能够进行模型数据访问的对象引用?这显然也是需要考虑的。
说了这么多,实际上就一个目的,实现一个针对基于注解的Controller的HandlerAdaptor的目的是明确的,那就是将请求信息绑定到具体的controller实例,然后调用相应的处理方法,并将返回结果以ModelAndView的形式返回给DispatcherServlet使用。但是,要实现这样一个 HandlerAdaptor的工作量和要考虑的关注点是很多的。所以,理解原理就好。如果要使用这样一个HandlerAdaptor实现类,还是使用Spring 2.5提供的官方支持吧!
Spring 2.5中为基于注解的Controller提供的HandlerAdaptor实现类是AnnotationMethodHandlerAdapter。该类为我们考虑并 实现了以上提到的几乎所有问题,甚至还要更全面。默认情况下,2.5版本的DispatcherServlet将在初始化的时候就实例化了一个AnnotationMethodHandlerAdapter,用于支持基于注解的 Controller。我想,如果没有对AnnotationMethodHandlerAdapter的定制需求的话,通常就不用在DispatcherServlet的webApplicationContext中明确地声明一个该类的bean定义了。
在Spring 5. 版本中,DispathcerServlet默认启用的HandlerAdapter有如下几个: