SpringMVC对自定义controller入参预处理方式

目录
  • Spring Mvc对自定义controller入参预处理
    • HandlerMethodArgumentResolver接口说明
    • 初学者一般喜欢类似下面的代码
    • 我们需要定义如下的一个参数分解器
    • 注册自定义分解器
  • SpringMVC技巧之通用Controller
    • 1. 前言
    • 2. 问题
    • 3. 解决方案
    • 4. 使用
    • 5. 特殊需求
    • 6. 完善

Spring Mvc对自定义controller入参预处理

在初学springmvc框架时,我就一直有一个疑问,为什么controller方法上竟然可以放这么多的参数,而且都能得到想要的对象,比如HttpServletRequest或HttpServletResponse,各种注解@RequestParam、@RequestHeader、@RequestBody、@PathVariable、@ModelAttribute等。相信很多初学者都曾经感慨过。

这篇文章就是讲解处理这方面内容的

我们可以模仿springmvc的源码,实现一些我们自己的实现类,而方便我们的代码开发。

HandlerMethodArgumentResolver接口说明

package org.springframework.web.method.support;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
public interface HandlerMethodArgumentResolver {
    //用于判定是否需要处理该参数分解,返回true为需要,并会去调用下面的方法resolveArgument。
    boolean supportsParameter(MethodParameter parameter);
    //真正用于处理参数分解的方法,返回的Object就是controller方法上的形参对象。
    Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
            NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception;
}

示例

本示例显示如何 优雅地将传入的信息转化成自定义的实体传入controller方法。

post 数据:

first_name = Bill

last_name = Gates

初学者一般喜欢类似下面的代码

package com.demo.controller;
import javax.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import com.demo.domain.Person;
import com.demo.mvc.annotation.MultiPerson;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Controller
@RequestMapping("demo1")
public class HandlerMethodArgumentResolverDemoController {
    @ResponseBody
    @RequestMapping(method = RequestMethod.POST)
    public String addPerson(HttpServletRequest request) {
        String firstName = request.getParameter("first_name");
        String lastName = request.getParameter("last_name");
        Person person = new Person(firstName, lastName);
        log.info(person.toString());
        return person.toString();
    }
}

这样的代码强依赖了javax.servlet-api的HttpServletRequest对象,并且把初始化Person对象这“活儿”加塞给了controller。代码显得累赘不优雅。在controller里我只想使用person而不想组装person,想要类似下面的代码:

@RequestMapping(method = RequestMethod.POST)
public String addPerson(Person person) {
  log.info(person.toString());
  return person.toString();
}

直接在形参列表中获得person。那么这该如实现呢?

我们需要定义如下的一个参数分解器

package com.demo.mvc.component;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import com.demo.domain.Person;
public class PersonArgumentResolver implements HandlerMethodArgumentResolver {
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.getParameterType().equals(Person.class);
    }
    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        String firstName = webRequest.getParameter("first_name");
        String lastName = webRequest.getParameter("last_name");
        return new Person(firstName, lastName);
    }
}

在supportsParameter中判断是否需要启用分解功能,这里判断形参类型是否为Person类,也就是说当形参遇到Person类时始终会执行该分解流程resolveArgument,也可以基于paramter上是否有我们指定的自定义注解判断是否需要流程分解。在resolveArgument中处理person的初始化工作。

注册自定义分解器

传统XML配置:

<mvc:annotation-driven>
      <mvc:argument-resolvers>
        <bean class="com.demo.mvc.component.PersonArgumentResolver"/>
      </mvc:argument-resolvers>
</mvc:annotation-driven>

<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
    <property name="customArgumentResolvers">
          <bean class="com.demo.mvc.component.PersonArgumentResolver"/>
    </property>
</bean>

spring boot java代码配置:

public class WebConfig extends WebMvcConfigurerAdapter{
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(new CustomeArgumentResolver());
    }
}

SpringMVC技巧之通用Controller

一个通用Controller。大多数情况下不再需要编写任何Controller层代码,将开发人员的关注点全部集中到Service层。

1. 前言

平时在进行传统的MVC开发时,为了完成某个特定的功能,我们通常需要同时编写Controller,Service,Dao层的代码。代码模式大概是这样的。

这里只贴出Controller层的代码,Service层也不是本次我们的关注点。

// ----------------------------------------- Controller层
@RestController
@RequestMapping("/a")
public class AController {
 @Resource(name = "aService")
 private AService aService;

 @PostMapping(value = "/a")
 public ResponseBean<String> a(HttpServletRequest request, HttpServletResponse response) {
  final String name = WebUtils.findParameterValue(request, "name");
  return ResponseBean.of(aService.invoke(name));
 }
}
// ----------------------------------------- 前端访问路径
// {{rootPath}}/a/a.do

2. 问题

只要有过几个月Java Web开发经验的,应该对这样的代码非常熟悉,熟悉到恶心。我们稍微注意下就会发现:上面的Controller代码中,大致做了如下事情:

收集前端传递过来的参数。

将第一步收集来的参数传递给相应的Service层的某个方法执行。

将Service层执行后的结果使用Controller层特有的ResponseBean进行封装后返回给前台。

所以我们在排除掉少有的特殊情况之后,就会发现在一般情况下这个所谓的Controller层的存在感实在有点稀薄。因此本文尝试去除掉这部分枯燥的重复性代码。

3. 解决方案

直接上代码。talk is cheap, show me the code。

// 这里之所以是 /lq , 而不是 /* ; 是因为 AntPathMatcher.combine 方法中进行合并时的处理, 导致 前一个 /* 丢失
/**
 * <p> 直接以前端传递来的Serivce名+方法名去调用Service层的同名方法; Controller层不再需要写任何代码
 * <p> 例子
 * <pre>
 *   前端: /lq/thirdService/queryTaskList.do
 *   Service层相应的方法签名:  Object queryTaskList(Map<String, Object> parameterMap)
 *   相应的Service注册到Spring容器中的id : thirdServiceService
 * </pre>
 * @author LQ
 *
 */
@RestController
@RequestMapping("/lq")
public class CommonController {
 private static final Logger LOG = LoggerFactory.getLogger(ThirdServiceController.class);
 @PostMapping(value = "/{serviceName}/{serviceMethodName}")
 public void common(@PathVariable String serviceName, @PathVariable final String serviceMethodName, HttpServletRequest request, HttpServletResponse response) {
  // 收集前台传递来的参数, 并作预处理
  final Map<String, String> parameterMap = HtmlUtils.getParameterMap(request);
  final Map<String, Object> paramsCopy = preDealOutParam(parameterMap);
  // 获取本次的调度服务名和相应的方法名
  //final List<String> serviceAndMethod = parseServiceAndMethod(request);
  //final String serviceName = serviceAndMethod.get(0) + "Service";
  //final String serivceMethodName = serviceAndMethod.get(1);

  // 直接使用Spring3.x新加入的@PathVariable注解; 代替上面的自定义操作
  serviceName = serviceName + "Service";
  final String fullServiceMethodName = StringUtil.format("{}.{}", serviceName, serivceMethodName);
  // 输出日志, 方便回溯
  LOG.debug("### current request method is [ {} ] ,  parameters is [ {} ]", fullServiceMethodName, parameterMap);
  // 获取Spring中注册的Service Bean
  final Object serviceBean = SpringBeanFactory.getBean(serviceName);
  Object rv;
  try {
   // 调用Service层的方法
   rv = ReflectUtil.invoke(serviceBean, serivceMethodName, paramsCopy);
   // 若用户返回一个主动构建的FriendlyException
   if (rv instanceof FriendlyException) {
    rv = handlerException(fullServiceMethodName, (FriendlyException) rv);
   } else {
    rv = returnVal(rv);
   }
  } catch (Exception e) {
   rv = handlerException(fullServiceMethodName, e);
  }
  LOG.debug("### current request method [ {} ] has dealed,  rv is [ {} ]", fullServiceMethodName, rv);
  HtmlUtils.writerJson(response, rv);
 }
 /**
  * 解析出Service和相应的方法名
  * @param request
  * @return
  */
 private List<String> parseServiceAndMethod(HttpServletRequest request) {
  // /lq/thirdService/queryTaskList.do 解析出 [ thirdService, queryTaskList ]
  final String serviceAndMethod = StringUtil.subBefore(request.getServletPath(), ".", false);
  List<String> split = StringUtil.split(serviceAndMethod, '/', true, true);
  return split.subList(1, split.size());
 }

 // 将传递来的JSON字符串转换为相应的Map, List等
 private Map<String, Object> preDealOutParam(final Map<String, String> parameterMap) {
  final Map<String, Object> outParams = new HashMap<String, Object>(parameterMap.size());
  for (Map.Entry<String, String> entry : parameterMap.entrySet()) {
   outParams.put(entry.getKey(), entry.getValue());
  }
  for (Map.Entry<String, Object> entry : outParams.entrySet()) {
   final String value = (String) entry.getValue();
   if (StringUtil.isEmpty(value)) {
    entry.setValue("");
    continue;
   }
   Object parsedObj = JSONUtil.tryParse(value);
   // 不是JSON字符串格式
   if (null == parsedObj) {
    continue;
   }
   entry.setValue(parsedObj);
  }
  return outParams;
 }
 // 构建成功执行后的返回值
 private Object returnVal(Object data) {
  return MapUtil.newMapBuilder().put("data", data).put("status", 200).put("msg", "success").build();
 }
 // 构建执行失败后的返回值
 private Object handlerException(String distributeMethod, Throwable e) {
  final String logInfo = StringUtil.format("[ {} ] fail", distributeMethod);
  LOG.error(logInfo, ExceptionUtil.getRootCause(e));
  return MapUtil.newMapBuilder().put("data", "").put("status", 500)
    .put("msg", ExceptionUtil.getRootCause(e).getMessage()).build();
 }
}

4. 使用

到此为止,Controller层的代码就算是完成了。之后的开发工作中,在绝大多数情况下,我们将不再需要编写任何Controller层的代码。只要遵循如下的约定,前端将会直接调取到Service层的相应方法,并获取到约定格式的响应值。

  • 前端请求路径 : {{rootPath}}/lq/serviceName/serviceMethodName.do
  • {{rootPath}} : 访问地址的根路径
  • lq :自定义的固定名称,用于满足SpringMVC的映射规则。
  • serviceName : 用于获取Spring容器中的Service Bean。这里的规则是 该名称后附加上Service字符来作为Bean Id来从Spring容器中获取相应 Service Bean。
  • serviceMethodName : 第三步中找到的Service Bean中的名为serviceMethodName的方法。签名为Object serviceMethodName(Map<String,Object> param)。

5. 特殊需求

对于有额外需要的特殊Controller,可以完全按照之前的Controller层写法。没有任何额外需要注意的地方。

6. 完善

上面的Service层的方法签名中,其参数使用的是固定的Map<String,Object> param。对Map和Bean的争论由来已久,经久不衰,这里不搅和这趟浑水。

对于希望使用Bean作为方法参数的,可以参考SpringMVC中对Controller层方法调用的实现,来达到想要的效果。具体的实现就不在这里献丑了,有兴趣的同学可以参考下源码ServletInvocableHandlerMethod.invokeAndHandle。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • 详解springMVC—三种控制器controller

    在springmvc中提供了三种controller的配置,1.针对不需要controller代码的,也就是只起到跳转页面的作用.2.可以接受实体类型的controller.3.可以接受表单数据的controller,它只允许POST提交,在配置文件中需要指定提交FORM,请求成功的FORM. 1.直接转发到页面,不需要添加controller代码. <bean id="toLogin" name="/toLogin.do" class="org.s

  • 关于Spring MVC在Controller层中注入request的坑详解

    前言 记一次为了节省代码没有在方法体中声明HttpServletRequest,而用autowire直接注入所钻的坑 结论:给心急的人. 直接在Controller的成员变量上使用@Autowire声明HttpServletRequest,这是线程安全的! @Controller public class TestController{ @Autowire HttpServletRequest request; @RequestMapping("/") public void test

  • 聊聊springmvc中controller的方法的参数注解方式

    绪论 相信接触过springmvc的同学都知道,在springmvc的控制层中,我们在方法的参数中可以使用注解标识.比如下面例子: public Map<String, Object> login(@PathVariable("loginParams") String loginParams) @PathVariable注解就标识了这个参数是作为一个请求地址模板变量的(不清楚的同学可以先学习一下restful设计风格).这些注解都是spring内置注解,那么 我们可不可以自

  • SpringMVC自定义参数绑定实现详解

    一.概述 1.3 参数绑定过程 1.2 @RequestParam 如果request请求的参数名和controller方法的形参数名称一致,适配器自动进行参数绑定.如果不一致可以通过 @RequestParam 指定request请求的参数名绑定到哪个方法形参上. 对于必须要传的参数,通过@RequestParam中属性required设置为true,如果不传此参数则报错. 对于有些参数如果不传入,还需要设置默认值,使用@RequestParam中属性defaultvalue设置默认值. 可以

  • Springmvc Controller接口代码示例

    Spring MVC Controller控制器,是MVC中的部分C,为什么是部分呢?因为此处的控制器主要负责功能处理部分: 收集.验证请求参数并绑定到命令对象: 将命令对象交给业务对象,由业务对象处理并返回模型数据: 返回ModelAndView(Model部分是业务对象返回的模型数据,视图部分为逻辑视图名). 1. 继承该接口 Controller接口,重写对应方法,或者采用注解Controller,自定义映射文件 @Controller @RequestMapping("/flight&q

  • SpringMVC对自定义controller入参预处理方式

    目录 Spring Mvc对自定义controller入参预处理 HandlerMethodArgumentResolver接口说明 初学者一般喜欢类似下面的代码 我们需要定义如下的一个参数分解器 注册自定义分解器 SpringMVC技巧之通用Controller 1. 前言 2. 问题 3. 解决方案 4. 使用 5. 特殊需求 6. 完善 Spring Mvc对自定义controller入参预处理 在初学springmvc框架时,我就一直有一个疑问,为什么controller方法上竟然可以放

  • SpringMVC实现Controller的三种方式总结

    目录 实现Controller的三种方式 1.实现Controller接口 2.实现HttpRequestHandler接口 3.全注解 关于SpringMVC的控制器(Controller) 控制器Controller 实现Controller的三种方式 1.实现Controller接口 实现Controller接口,重写handleRequest方法,ModelAndView对象是一个模型视图对象,既可以添加数据,又可以保存页面信息,并且处理请求的方式是转发.这个对象要拆成两部分来看 mod

  • springmvc前台向后台传值几种方式总结(从简单到复杂)

    1. 基本数据类型(以int为例,其他类似): Controller代码: @RequestMapping("saysth.do") public void test(int count) { } 表单代码: <form action="saysth.do" method="post"> <input name="count" value="10" type="text"

  • SpringMVC返回json数据的三种方式

    Spring MVC属于SpringFrameWork的后续产品,已经融合在Spring Web Flow里面.Spring 框架提供了构建 Web 应用程序的全功能 MVC 模块.使用 Spring 可插入的 MVC架构,从而在使用Spring进行WEB开发时,可以选择使用Spring的SpringMVC框架或集成其他MVC开发框架,如Struts1,Struts2等. 1.第一种方式是spring2时代的产物,也就是每个json视图controller配置一个Jsoniew. 如:<bean

  • 详解springmvc 接收json对象的两种方式

    最近学习了springmvc 接收json对象的两种方式,现在整理出来,具体如下: 1.以实体类方式接收 前端 ajax 提交数据: function fAddObj() { var obj = {}; obj['objname'] = "obj"; obj['pid'] = 1 ; $.ajax({ url: 'admin/Obj/addObj.do', method: 'post', contentType: 'application/json', // 这句不加出现415错误:U

  • SpringMVC编程使用Controller接口实现控制器实例代码

    Controller简介 Controller控制器,是MVC中的部分C,为什么是部分呢?因为此处的控制器主要负责功能处理部分: 1.收集.验证请求参数并绑定到命令对象: 2.将命令对象交给业务对象,由业务对象处理并返回模型数据: 3.返回ModelAndView(Model部分是业务对象返回的模型数据,视图部分为逻辑视图名). DispatcherServlet,主要负责整体的控制流程的调度部分: 1.负责将请求委托给控制器进行处理: 2.根据控制器返回的逻辑视图名选择具体的视图进行渲染(并把

  • springmvc+shiro自定义过滤器的实现代码

    实现需求: 1.用户未登录,跳转到登录页,登录完成后会跳到初始访问页. 2.用户自定义处理(如需要激活),跳转到激活页面,激活完成后会跳到初始访问页. 使用到的框架 springmvc 的拦截器 shiro 自定义过滤器 实现: 1.编写拦截器通过session保存初始访问的页面地址,便于后面回跳这个页面做准备. import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.serv

  • SpringMVC Json自定义序列化和反序列化的操作方法

    需求背景 需求一:SpringMVC构建的微服务系统,数据库对日期的存储是Long类型的时间戳,前端之前是默认使用Long类型时间,现在前端框架改动,要求后端响应数据时,Long类型的时间自动变成标准时间格式(yyyy-MM-dd HH:mm:ss). 涉及到这个转换的范围挺大,所有的实体表都有创建时间createTime和修改时间updateTime,目前的主要诉求也是针对这两个字段,并且在实体详情数据和列表数据都存在,需要一个统一的方法,对这两个字段进行处理. 需求二:前端请求上传的JSON

  • springMVC不扫描controller中的方法问题

    目录 springMVC不扫描controller 下面是正确的spring-mvc.xml文件 那我遇到这个问题的原因是什么呢? springMVC包扫描问题 为什么@COntroller要放在springMVC中? springMVC不扫描controller 最近把之前的一个Maven项目在一个新的电脑环境上导入Eclipse,启动时却发现不扫描 controller 中的方法 下面是正确的 spring-mvc.xml 文件 <?xml version="1.0" enc

  • Java SpringMVC实现自定义拦截器

    目录 SpringMVC实现自定义拦截器 1拦截器(interceptor)的作用 2拦截器和过滤器区别 3.实现过程 3.1创建拦截器类实现HandlerInterceptor接口 3.2配置拦截器 3.3测试拦截器的拦截效果 3.4编写jsp页面 3.5测试结果 4.拦截器链 5.知识小结 总结 SpringMVC实现自定义拦截器 1 拦截器(interceptor)的作用 Spring MVC 的拦截器类似于 Servlet 开发中的过滤器 Filter,用于对处理器进行预处理和后处理.

随机推荐