传送门
如何看待写代码这件事
最近在公司写代码(包括看代码),突然有一点小小的感叹。好多人整天研究什么高并发,高可用,分布式,开口架构闭口新技术,就是不愿意花时间把自己的JAVA代码写的好一点。
把代码写好就是给自己印的最好的名片,也是对同事最大的负责!
可惜好多人不这样认为,或者说可能是现在环境就是这样吧:面试各种的造火箭,考算法导致从业者只能投其所好,刷题/刷各种高大上的所谓架构技术,而忽视一个最本质的前提!
那就是技术是为业务服务的,绝大部分的公司是用不上所谓的大厂架构的,强行匹配只会适得其反。
有空还是多琢磨琢磨怎么把那点JAVA代码写的更好吧。
这里的说法有点属于"夹带私货"了,太过片面了,切勿对号入座。
什么是模板方法
在很早以前(真的是很早了,看了下发布日期是2018年2月!),学习过模板方法,它属于常用的设计中的一种。当时里面介绍的例子取自《Head First设计模式》,所以文章算作是翻译过来的。例子比较简单,实现也是用的继承+多态的。而还有一种很常用的模板方法,就是一个类+静态方法,使用者直接通过静态方法来调用!
Template.execute(a,b);
- Template为类名
- execute为方法
- a,b为参数
在上面的调用中,Template.execute是不会变化,顾名思义就是模板方法的意思。而a,b则是需要调用方传递的参数,必须是模板规定的类型。下面就以一个实际场景来看看如何抽象一个模板方法。
接口调用场景
对于JAVA程序员来说,spring肯定绕不开的结。当需要写一个后端接口的时候,通过springMVC可以很方便的来实现,比如在Oauth2系列7:授权码和访问令牌的颁发流程是怎样实现的?里面提到的准备工作-验证基本信息:
@RestController
@RequestMapping("/auth")
public class OauthController
{
@GetMapping("/authorize")
public void authorize(@RequestParam("response_type") String responseType, @RequestParam("client_id") String clientId,
@RequestParam("redirect_uri") String redirectUri, String scope)
{
}
}
还有验证客户端-生成访问令牌:
@PostMapping("/token")
public TokenModel getToken(@RequestBody GetTokenRequest getTokenRequest) {
// 获取令牌前置检验
preGetTokenCheck(getTokenRequest);
// 检验授权码
checkCode(getTokenRequest.getCode());
// 生成t访问令牌
TokenModel tokenModel = generateToken();
return tokenModel;
}
private TokenModel generateToken() {
// 获取code信息,比如从redis
// CodeModel codeModel = getCode;
TokenModel tokenModel = new TokenModel();
tokenModel.setAccessToken(UUID.randomUUID().toString());
tokenModel.setExpiresIn(3600);
tokenModel.setRefreshToken(UUID.randomUUID().toString());
// tokenModel.setScope(codeModel.getScope);
return tokenModel;
}
- 在类上打上注解RestController或Controller,现在一般自动转换json就用RestController
- 在方法上打上注解RequestMapping或GetMapping/PostMapping等,表示这个是一个接口方法
- 在方法里面打上注解RequestParam或PathVariable,用来获取参数
大致按照这3个步骤来操作,剩下的主要就是业务代码编写了(实际项目里面也没有什么大的区别)。一般项目会分层,简单的就三层:Web/Service/Dao。
- Web表示展示层:接口的入参获取,参数检验;日志打印;响应转换/返回(包括异常处理)
- Service就是业务层:处理业务逻辑的,是方法的主体代码
- Dao层称为存储层:一般表示db处理,也可以是其它持久操作
模板方法
根据上面简单三层的理解,定义一个模板方法出来:
@Slf4j
public class WebTemplate
{
public static String execute(String req)
{
try
{
// 1:打印入参
log.info("方法参数:{}", req);
// 2:参数检验
// TODO
// 3:业务方法
}
catch (Exception e)
{
// 4:异常处理
return "fail";
}
return "success";
}
}
这里定义了模板方法的步骤:
- 入参类型(如果是上面的例子只能的String肯定有局限性),最好支持泛型,比如都继承Request基类
- 参数打印:将参数都打印出来,方便统计排查
- 参数检验:对输入参数进行检验,如果不符合条件则抛出异常,让步骤4异常来统一处理
- 业务方法执行:对于这种业务方法的执行,可以定义一个接口让调用方来实现
- 异常执行:异常可以分为业务/全局异常,进行统一的处理,直接抛出或转换成对应异常码
- 组装响应:响应可以自定义,比如如上的异常码/异常信息
- 后置处理:可以在方法结束时,进行需要的后置处理,比如打印日志,方便后续监控
由此可见一个完整模板方法类似如下:
package com.tw.tsm.base.template;
import com.tw.tsm.base.request.BaseRequestDTO;
import com.tw.tsm.base.response.BaseResponseDTO;
import lombok.extern.slf4j.Slf4j;
/**
* 模板方法类
*/
@Slf4j
public class WebTemplate
{
/**
* 模板方法
* @param req 请求参数
* @param res 响应参数
* @param callback 回调方法
* @param <T>
* @param <R>
*/
public static <T extends BaseRequestDTO, R extends BaseResponseDTO> void execute(T req, R res, ServiceCallback callback)
{
try
{
// 1:打印入参
log.info("方法参数:{}", req);
// 2:参数检验
callback.check(req);
// 3:业务方法
callback.doService(req);
}
catch (Exception e)
{
// 4:异常处理
}
finally
{
log.info("处理结果:{}", res);
}
}
}
/**
* 请求基类
*/
public class BaseRequestDTO
{
}
/**
* 响应基类
*/
@Data
public class BaseResponseDTO<T>
{
/** 错误码 */
private String code;
/** 错误信息 */
private String msg;
/** 返回内容 */
private T data;
}
/**
* 模板处理接口
*
* @param <T>
*/
public interface ServiceCallback<T>
{
void check(T req);
void doService(T req);
}
至此,模板方法已经初具雏形,对于调用方来说,即可如下:
WebTemplate.execute(req, new BaseResponseDTO(), new ServiceCallback() {
@Override
public void check(Object req) {
// 参数检验
}
@Override
public void doService(Object req) {
// 业务方法
}
});
异常处理
上面的模板方法,统一捕获了异常Exception,这样所有的异常都会被处理。不过在很多时候,可能需要对异常进行分类处理,比如将异常分为业务/系统异常:
- 对于业务异常希望明确提示调用方,告诉它失败的原因:比如参数不合法,重复提交,资源不存在等
- 对于系统异常,比如db连接超时,第三方服务不可用,Npe异常等又不希望直接返回给调用者:因为这种错误一般不是逻辑错误,调用者感知了也解决不了;再者可能会暴露代码堆栈信息,也不太安全。所以这种一般处理是返回默认的提示,比如"系统繁忙,请稍候重试"
基于此,定义一个异常类:CheckedException让它继承自RuntimeException表示业务异常,跟Exception做区分:
/**
* 检验异常
*/
@Data
@RequiredArgsConstructor
public class CheckedException extends RuntimeException
{
/** 错误码 */
private String code;
/** 错误信息 */
private String msg;
}
在模板方法里面,加入CheckException处理:
/**
* 模板方法类
*/
@Slf4j
public class WebTemplate
{
/**
* 模板方法
* @param req 请求参数
* @param res 响应参数
* @param callback 回调方法
* @param <T>
* @param <R>
*/
public static <T extends BaseRequestDTO, R extends BaseResponseDTO> void execute(T req, R res, ServiceCallback callback)
{
try
{
// 1:打印入参
log.info("方法参数:{}", req);
// 2:参数检验
callback.check(req);
// 3:业务方法
callback.doService(req);
}
catch (Exception e)
{
// 4:异常处理
handleException(e, res);
}
finally
{
log.info("处理结果:{}", res);
}
}
/**
* 异常处理
* @param e
* @param response
*/
private static void handleException(Exception e, BaseResponseDTO response)
{
if (e instanceof CheckedException)
{
CheckedException ex = (CheckedException)e;
ex.setCode(ex.getCode());
ex.setMsg(ex.getMsg());
return;
}
// 设置默认异常
// TODO
}
}
异常码
在模板类中单独定义了一个方法handleException来处理异常:如果是业务异常,则将异常中的错误码/错误信息填充到响应里面,如果是系统异常则填充默认异常。因为这里定义的只有一个异常类CheckException来表示所有的业务异常,所以对于业务异常需要单独定义一个类来表示不同的场景:
/**
* 通用错误码
*/
@AllArgsConstructor
public enum ComErrorCode
{
/** 缺少参数 */
PARAM_MISS("E01000", "缺少参数"),
/** 系统繁忙,请稍候重试 */
SYSTEM_ERROR("E01999", "系统繁忙,请稍候重试");
@Getter
private String code;
@Getter
private String msg;
}
模板方法补充如下:
/**
* 模板方法类
*/
@Slf4j
public class WebTemplate
{
/**
* 模板方法
* @param req 请求参数
* @param res 响应参数
* @param callback 回调方法
* @param <T>
* @param <R>
*/
public static <T extends BaseRequestDTO, R extends BaseResponseDTO> void execute(T req, R res, ServiceCallback callback)
{
try
{
// 1:打印入参
log.info("方法参数:{}", req);
// 2:参数检验
callback.check(req);
// 3:业务方法
callback.doService(req);
}
catch (Exception e)
{
// 4:异常处理
handleException(e, res);
}
finally
{
log.info("处理结果:{}", res);
}
}
/**
* 异常处理
* @param e
* @param response
*/
private static void handleException(Exception e, BaseResponseDTO response)
{
if (e instanceof CheckedException)
{
CheckedException ex = (CheckedException)e;
ex.setCode(ex.getCode());
ex.setMsg(ex.getMsg());
return;
}
// 设置默认异常
response.setCode(ComErrorCode.SYSTEM_ERROR.getCode());
response.setMsg(ComErrorCode.SYSTEM_ERROR.getMsg());
}
}
lambda在模板方法中的应用
既然是要用lambda表达式在模板方法中应用,所以就不能像刚才那样对于回调函数直接用匿名类,这里就改造一下:
/**
* 模板回调函数
*
* @param <T>
*/
@FunctionalInterface
public interface ServiceCallback<T>
{
default void check(T req)
{
}
void doService(T req);
}
WebTemplate.execute(req, new BaseResponseDTO(), request -> {
});
- 首先改造一下回调函数,增加@FunctionalInterface注解,表示这是一个函数式接口
- 将check()方法声明为default,这里java8的默认方法
- 最后用lambda实现业务处理逻辑