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代码中,大致做了如下事情:
1. 收集前端传递过来的参数。
2. 将第一步收集来的参数传递给相应的Service层的某个方法执行。
3. 将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
1. {{rootPath}} : 访问地址的根路径
2. lq :自定义的固定名称,用于满足SpringMVC的映射规则。
3. serviceName : 用于获取Spring容器中的Service Bean。这里的规则是 该名称后附加上Service字符来作为Bean Id来从Spring容器中获取相应 Service Bean。
4. serviceMethodName : 第三步中找到的Service Bean中的名为serviceMethodName的方法。签名为Object serviceMethodName(Map<String,Object> param)

5. 特殊Controller

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

6. 完善

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

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

猜你喜欢

转载自blog.csdn.net/lqzkcx3/article/details/80454826