前言
最近优化代码,总结了一点个人的开发经验。由于我这人写代码喜欢偷懒,奉承着能少写一行代码,绝不多写一行的宗旨开发。
一. 一行代码的初衷
增删改查大量的业务代码,充斥着setter和getter,一个函数中仅仅只有一处地方还好,多了就会让代码看起了很浑厚,不简洁;
对接了大量的第三方接口,如果要详细的设计每一个接口,势必会造成代码的大量冗余,不简洁;
大量不同的service有相同的方法,仅仅是入参和返回结果不同,做着重复的工作,时间久了就不耐烦了。
二. 思考如何简化实现
每次打印日志时,我们都会定义一个log,然后通过这个log能打印各种异常,然后我就在想,它是怎么实现,怎么就能一行代码就把日志打印出来。然后我各种翻源码,没找到个所以然来,干脆放弃。
但是此时我注意到了枚举,一切都要从这个枚举说起。
三. 实现
建造者模式。建造者模式可以优化系统中大量的setter,使代码变的十分简洁。实现也很简单,如果你想偷懒,可以直接使用Lombok包中@Builder注解即可。
定义第三方接口的请求接口方法,主要重心放在处理返回结果中。
使用枚举处理第三方提供的服务
四. 实例
这里着重讲解使用枚举一行代码处理第三方提供的服务。
系统日志保存到message-send服务的es服务器中
正常来说,请求第三方服务,肯定要通过第三方服务提供的地址,访问他们的接口。这里我们不得不定义一个函数专门用来处理这些操作,然后再在需要的地方调用这个函数。这里又要涉及很多东西,比如每次
请求都要创建httpClient,区分日志的类型,每种日志类型的打印的参数不一致,参数个数也不一致,导致参数模板可能就需要好几个。打印日志报错影响到主业务流程怎么办等等一些列问题。已开始我尝试用了这种
方式处理,发现代码量有点大,要定义很多东西。所以我使用了枚举,具体方法如下:
a. 定义ES服务客户端 ElasticClient,这个客户端的主要作用就是封装请求es服务的接口以及请求参数
@Data
public class ElasticClient implements ElasticRequestApi {
private static final Logger log = LoggerFactory.getLogger(ElasticClient.class);
/**
* 请求uri
*/
private String uri;
/**
* 用户密匙
*/
private String appKey;
/**
* 用户秘钥
*/
private String secret;
/**
* 项目名称
*/
private String projectName;
/**
* rest客户端
*/
private RestTemplate restTemplate;
/**
* 日志id
*/
private String logId;
/**
* 日志等级
*/
private String logLevel;
/**
* 打印时间
*/
private String printTime;
/**
* 系统环境
*/
private String env;
ElasticClient(ElasticClient.Builder builder) {
this.appKey = builder.appKey;
this.secret = builder.secret;
this.uri = builder.uri;
this.restTemplate = builder.restTemplate;
this.projectName = builder.project;
this.logId = builder.logId;
this.logLevel = builder.logLevel;
this.logLevel = builder.logLevel;
this.printTime = builder.printTime;
this.env = builder.env;
}
@Override
public void executePostSaveLog(String logLevel, String message) {
List<LogInfoEntity> logInfoEntities = new ArrayList<>();
LogInfoEntity logInfoEntity = new LogInfoEntity();
String logId = parseStringByFormat(new Date(), "yyyyMMddHHmmssSSS") + RandomUtils.nextInt(10000, 99999);
logInfoEntity.setLogId(this.projectName + "-" + logId);
logInfoEntity.setLogLevel(logLevel);
logInfoEntity.setLogMsg(message.substring(0, Math.min(30000, message.length())));
logInfoEntity.setProjectName(this.projectName);
logInfoEntity.setPrintTime(parseStringByFormat(new Date(), "yyyy-MM-dd HH:mm:ss"));
HttpHeaders headers = new HttpHeaders();
logInfoEntities.add(logInfoEntity);
SaveEntityForm form = new SaveEntityForm();
form.setLogEntities(logInfoEntities);
HttpEntity<SaveEntityForm> httpEntity = new HttpEntity<>(form, headers);
String uri = this.uri + "es/log/commit/" + this.env;
ResponseEntity<Object> exchange = restTemplate.exchange(uri, HttpMethod.POST, httpEntity, Object.class);
if (exchange.getStatusCodeValue() != 200) {
log.error("保存日志到es失败 res -> {}", exchange);
}
}
public static void main(String[] args) {
System.out.println("ssss".getBytes(StandardCharsets.UTF_8).length);
System.out.println("ssss".length());
}
public static String parseStringByFormat(Date date, String format) {
if (date == null || StringUtils.isBlank(format)) {
throw new BizException("日期转化参数不能为空");
}
SimpleDateFormat sdf = new SimpleDateFormat(format);
return sdf.format(date);
}
@Override
public LogInfoEntity executeGetLog(String logId) {
ResponseEntity<LogInfoEntity> forEntity = restTemplate.getForEntity(this.uri + "/getLog/" + logId, LogInfoEntity.class);
if (forEntity.getStatusCodeValue() == 200) {
return forEntity.getBody();
}
return null;
}
public ElasticClient logLevel(String logLevel) {
this.logLevel = logLevel;
return this;
}
public static final class Builder {
private String uri;
private String appKey;
private String secret;
private RestTemplate restTemplate;
private String project;
private String logId;
private String logLevel;
private String printTime;
private String env;
public ElasticClient.Builder uri(String uri) {
this.uri = uri;
return this;
}
public ElasticClient.Builder appKey(String appKey) {
this.appKey = appKey;
return this;
}
public ElasticClient.Builder secret(String secret) {
this.secret = secret;
return this;
}
public ElasticClient.Builder restTemplate(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
return this;
}
public ElasticClient.Builder project(String project) {
this.project = project;
return this;
}
public ElasticClient.Builder logId(String logId) {
this.logId = logId;
return this;
}
public ElasticClient.Builder logLevel(String logLevel) {
this.logLevel = logLevel;
return this;
}
public ElasticClient.Builder printTime(String printTime) {
this.printTime = printTime;
return this;
}
public ElasticClient.Builder env(String env) {
this.env = env;
return this;
}
public Builder() {
this.appKey = "";
this.secret = "";
this.uri = "http://localhost:8090/";
this.restTemplate = new RestTemplate();
this.project = "ALL";
this.logId = this.project + "-" + UUID.randomUUID().toString().replaceAll("-", "");
this.logLevel = "INFO";
this.printTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
this.env = "dev";
}
public Builder(ElasticClient elasticClient) {
this.uri = elasticClient.uri;
this.appKey = elasticClient.appKey;
this.secret = elasticClient.secret;
this.restTemplate = elasticClient.restTemplate;
this.project = elasticClient.projectName;
this.logId = elasticClient.logId;
this.logLevel = elasticClient.logLevel;
this.printTime = elasticClient.printTime;
this.env = elasticClient.env;
}
public ElasticClient build() {
return new ElasticClient(this);
}
}
}
b. 创建EliasticClient配置ElasticClientConfig,用于注入es客户端以及httpClient
@Configuration
public class ElasticClientConfig {
@Resource
private ElasticLogProperties elasticLogProperties;
@Bean
public HttpClientConnectionManager httpClientConnectionManager() {
PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager();
poolingHttpClientConnectionManager.setMaxTotal(500);
poolingHttpClientConnectionManager.setDefaultMaxPerRoute(50);
return poolingHttpClientConnectionManager;
}
@Bean
public HttpClient httpClient(HttpClientConnectionManager httpClientConnectionManager) {
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
httpClientBuilder.setConnectionManager(httpClientConnectionManager);
return httpClientBuilder.build();
}
@Bean
public ClientHttpRequestFactory clientHttpRequestFactory(HttpClient httpClient) {
HttpComponentsClientHttpRequestFactory httpComponentsClientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory();
httpComponentsClientHttpRequestFactory.setHttpClient(httpClient);
httpComponentsClientHttpRequestFactory.setConnectTimeout(20 * 1000);
httpComponentsClientHttpRequestFactory.setReadTimeout(20 * 1000);
httpComponentsClientHttpRequestFactory.setConnectionRequestTimeout(20 * 1000);
return httpComponentsClientHttpRequestFactory;
}
@Bean("restTemplateForEs")
public RestTemplate restTemplate(ClientHttpRequestFactory clientHttpRequestFactory) {
RestTemplate restTemplate = new RestTemplate();
restTemplate.setRequestFactory(clientHttpRequestFactory);
return restTemplate;
}
@Bean
public ElasticClient elasticClient(@Qualifier("restTemplateForEs") RestTemplate restTemplate) {
return new ElasticClient.Builder()
.restTemplate(restTemplate)
.uri(elasticLogProperties.getUri())
.project(elasticLogProperties.getProjectName())
.env(elasticLogProperties.getEnv())
.build();
}
}
c. 定义访问es服务的接口,ElasticClient实现这个接口,并提供具体的实现
public interface ElasticRequestApi {
/**
* 保存日志到es
*
* @param logLevel 日志级别
* @param message 日志
*/
void executePostSaveLog(String logLevel, String message);
/**
* 获取日志信息
*
* @param logId 日志id
* @return 日志信息
*/
LogInfoEntity executeGetLog(String logId);
}
/**
* es客户端
* <p>
* 构建请求日志client,封装请求接口
* 可通过@Bean配置交由spring管理,具体配置可参见{@link com.cheapp.work2.common.elastic.ElasticClientConfig}
* </p>
* <p>
* Use: 通过{@link com.cheapp.work2.common.elastic.Log}获取日志等级,然后调用方法{@link com.cheapp.work2.common.elastic.Log#print(Logger, String)} ()}打印日志。
* 该方法通过参数Logger打印对应的日志信息并调用{@link com.cheapp.work2.common.elastic.Log#execute(String)}请求
* {@link com.cheapp.work2.common.elastic.ElasticClient#executePostSaveLog(String, String)}接口,保存日志到es。
* </p>
*
* @author baiye
* @since 2021/6/4 5:48 下午
**/
@Data
public class ElasticClient implements ElasticRequestApi {
private static final Logger log = LoggerFactory.getLogger(ElasticClient.class);
/**
* 请求uri
*/
private String uri;
/**
* 用户密匙
*/
private String appKey;
/**
* 用户秘钥
*/
private String secret;
/**
* 项目名称
*/
private String projectName;
/**
* rest客户端
*/
private RestTemplate restTemplate;
/**
* 日志id
*/
private String logId;
/**
* 日志等级
*/
private String logLevel;
/**
* 打印时间
*/
private String printTime;
/**
* 系统环境
*/
private String env;
ElasticClient(ElasticClient.Builder builder) {
this.appKey = builder.appKey;
this.secret = builder.secret;
this.uri = builder.uri;
this.restTemplate = builder.restTemplate;
this.projectName = builder.project;
this.logId = builder.logId;
this.logLevel = builder.logLevel;
this.logLevel = builder.logLevel;
this.printTime = builder.printTime;
this.env = builder.env;
}
@Override
public void executePostSaveLog(String logLevel, String message) {
List<LogInfoEntity> logInfoEntities = new ArrayList<>();
LogInfoEntity logInfoEntity = new LogInfoEntity();
String logId = parseStringByFormat(new Date(), "yyyyMMddHHmmssSSS") + RandomUtils.nextInt(10000, 99999);
logInfoEntity.setLogId(this.projectName + "-" + logId);
logInfoEntity.setLogLevel(logLevel);
logInfoEntity.setLogMsg(message.substring(0, Math.min(30000, message.length())));
logInfoEntity.setProjectName(this.projectName);
logInfoEntity.setPrintTime(parseStringByFormat(new Date(), "yyyy-MM-dd HH:mm:ss"));
HttpHeaders headers = new HttpHeaders();
logInfoEntities.add(logInfoEntity);
SaveEntityForm form = new SaveEntityForm();
form.setLogEntities(logInfoEntities);
HttpEntity<SaveEntityForm> httpEntity = new HttpEntity<>(form, headers);
String uri = this.uri + "es/log/commit/" + this.env;
ResponseEntity<Object> exchange = restTemplate.exchange(uri, HttpMethod.POST, httpEntity, Object.class);
if (exchange.getStatusCodeValue() != 200) {
log.error("保存日志到es失败 res -> {}", exchange);
}
}
public static void main(String[] args) {
System.out.println("ssss".getBytes(StandardCharsets.UTF_8).length);
System.out.println("ssss".length());
}
public static String parseStringByFormat(Date date, String format) {
if (date == null || StringUtils.isBlank(format)) {
throw new BizException("日期转化参数不能为空");
}
SimpleDateFormat sdf = new SimpleDateFormat(format);
return sdf.format(date);
}
@Override
public LogInfoEntity executeGetLog(String logId) {
ResponseEntity<LogInfoEntity> forEntity = restTemplate.getForEntity(this.uri + "/getLog/" + logId, LogInfoEntity.class);
if (forEntity.getStatusCodeValue() == 200) {
return forEntity.getBody();
}
return null;
}
public ElasticClient logLevel(String logLevel) {
this.logLevel = logLevel;
return this;
}
public static final class Builder {
private String uri;
private String appKey;
private String secret;
private RestTemplate restTemplate;
private String project;
private String logId;
private String logLevel;
private String printTime;
private String env;
public ElasticClient.Builder uri(String uri) {
this.uri = uri;
return this;
}
public ElasticClient.Builder appKey(String appKey) {
this.appKey = appKey;
return this;
}
public ElasticClient.Builder secret(String secret) {
this.secret = secret;
return this;
}
public ElasticClient.Builder restTemplate(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
return this;
}
public ElasticClient.Builder project(String project) {
this.project = project;
return this;
}
public ElasticClient.Builder logId(String logId) {
this.logId = logId;
return this;
}
public ElasticClient.Builder logLevel(String logLevel) {
this.logLevel = logLevel;
return this;
}
public ElasticClient.Builder printTime(String printTime) {
this.printTime = printTime;
return this;
}
public ElasticClient.Builder env(String env) {
this.env = env;
return this;
}
public Builder() {
this.appKey = "";
this.secret = "";
this.uri = "http://localhost:8090/";
this.restTemplate = new RestTemplate();
this.project = "ALL";
this.logId = this.project + "-" + UUID.randomUUID().toString().replaceAll("-", "");
this.logLevel = "INFO";
this.printTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
this.env = "dev";
}
public Builder(ElasticClient elasticClient) {
this.uri = elasticClient.uri;
this.appKey = elasticClient.appKey;
this.secret = elasticClient.secret;
this.restTemplate = elasticClient.restTemplate;
this.project = elasticClient.projectName;
this.logId = elasticClient.logId;
this.logLevel = elasticClient.logLevel;
this.printTime = elasticClient.printTime;
this.env = elasticClient.env;
}
public ElasticClient build() {
return new ElasticClient(this);
}
}
}
d. 定义日志枚举,并实现打印日志和上传日志到es的方法,由于枚举类中无法直接注入服务,所以采用内部类的方式加载服务
public enum Log {
DEBUG,
INFO,
WARN,
ERROR,
;
private static final Logger log = LoggerFactory.getLogger(Log.class);
private ElasticClient elasticClient;
private NacosDynamicProperties nacosDynamicProperties;
public void print(Logger log, String msg) {
if (nacosDynamicProperties.isEsLogPrintOn()) {
threadPoolExecutor.execute(() -> execute(msg));
}
switch (this.name()) {
case "DEBUG":
log.debug(msg);
break;
case "INFO":
log.info(msg);
break;
case "WARN":
log.warn(msg);
break;
case "ERROR":
log.error(msg);
break;
}
}
public void print(Logger log, String v, Object... obj) {
if (nacosDynamicProperties.isEsLogPrintOn()) {
String msg = getMsg(v, obj);
threadPoolExecutor.execute(() -> execute(msg));
}
switch (this.name()) {
case "DEBUG":
log.debug(v, obj);
break;
case "INFO":
log.info(v, obj);
break;
case "WARN":
log.warn(v, obj);
break;
case "ERROR":
log.error(v, obj);
break;
}
}
private static String getMsg(String v, Object... objects) {
String[] split = v.split("\\{}");
StringBuilder sb = new StringBuilder();
for (int i = 0; i < split.length; i++) {
if (i > objects.length - 1) {
sb.append(split[i]);
continue;
}
sb.append(split[i]).append(objects[i]);
}
return sb.toString();
}
protected void execute(String msg) {
elasticClient.logLevel(this.name()).executePostSaveLog(this.name(), msg);
}
@Component
public static class ServiceInjector {
public ServiceInjector(@Qualifier("elasticClient") ElasticClient elasticClient,
@Qualifier("nacosDynamicProperties") NacosDynamicProperties nacosDynamicProperties) {
log.info("init log service!");
DEBUG.elasticClient = elasticClient;
DEBUG.nacosDynamicProperties = nacosDynamicProperties;
INFO.elasticClient = elasticClient;
INFO.nacosDynamicProperties = nacosDynamicProperties;
WARN.elasticClient = elasticClient;
WARN.nacosDynamicProperties = nacosDynamicProperties;
ERROR.elasticClient = elasticClient;
ERROR.nacosDynamicProperties = nacosDynamicProperties;
}
}
private static final int availableProcessors = Runtime.getRuntime().availableProcessors();
private static final ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
availableProcessors,
20,
100L,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>());
e. 使用如下图,我们只需要像正常的日志使用即可,在打印日志的同时也会把日志上传到es服务器,即使上传失败报错也不会影响主流程业务,这里要注意尽量在业务代码中使用,如果能明确知道配置的加载顺序,保证ESClient在之前加载,也可在初始化配置中使用。
- aliyun短信发送服务
这里直接描述怎么用枚举一行代码发送短信
a. 配置一个短信模板的枚举,枚举包括短信模板code和短信参数
public enum AliMessage {
/**
* 登录注册短信通知
*/
LOGIN_REG_TEMPLATE("", "{\"code\":\"%s\"}"),
/**
* 邀请新员工短信模板
*/
INVITE_NEW_STAFF_TEMPLATE("", "{\"corpName\":\"%s\"}"),
/**
* 预约参观短信模板
*/
VISIT_TEMPLATE("", "{\"username\": \"%s\", \"phoneNo\":\"%s\", \"remark\":\"%s\"}"),
/**
* 报修短信模板
*/
REPAIR_TEMPLATE("", "{\"username\": \"%s\", \"phoneNo\":\"%s\", \"repairType\": \"%s\", \"repairDesc\":\"%s\"}"),
/**
* 企业管理员变更通知
*/
CORP_ACCOUNT_TEMPLATE("", "{\"corpName\":\"%s\",\"adminNo\":\"%s\", \"password\":\"%s\"}"),
/**
* 客户租赁场地到期通知
*/
CONTRACT_EXPIRE_TEMPLATE("", ""),
/**
* 缴款单出账通知
*/
BILL_TEMPLATE("", "{\"billName\":\"%s\",\"adminNo\":\"%s\"}"),
/**
* 预约参观办公室短信模板
*/
VISIT_OFFICE_TEMPLATE("", "{\"username\": \"%s\", \"phoneNo\":\"%s\", \"officeName\":\"%s\", \"remark\":\"%s\"}"),
;
private String tempCode;
private String paramJson;
AliMessage() {
}
AliMessage(String tempCode, String paramJson) {
this.tempCode = tempCode;
this.paramJson = paramJson;
}
private static final Logger log = LoggerFactory.getLogger(AliMessage.class);
private AliMessageService aliMessageService;
public Boolean send(String phoneNo, Object... objs) {
Object[] no = new Object[objs.length];
if (objs.length > 0) {
for (int i = 0; i < objs.length; i++) {
String str = objs[i].toString();
if (str.length() > 25) {
str = str.substring(0, 22) + "...";
}
no[i] = str;
}
}
SendMessageForm form = new SendMessageForm();
form.setPhoneNo(phoneNo);
form.setParamJson(StringUtils.isNotBlank(this.paramJson) ? String.format(this.paramJson, no) : "");
form.setTemplateCode(this.tempCode);
SendMessageVo sendMessageVo = aliMessageService.sendMessage(form);
log.info("短信发送信息 -> {}", sendMessageVo);
if (!"OK".equals(sendMessageVo.getCode())) {
throw Result.toBizException(ResultCodeEnum.SEND_MESSAGE_FAIL, sendMessageVo.getMessage());
}
return true;
}
@Component
public static class ServiceInjector {
public ServiceInjector(AliMessageService aliMessageService) {
LOGIN_REG_TEMPLATE.aliMessageService = aliMessageService;
INVITE_NEW_STAFF_TEMPLATE.aliMessageService = aliMessageService;
VISIT_TEMPLATE.aliMessageService = aliMessageService;
REPAIR_TEMPLATE.aliMessageService = aliMessageService;
CORP_ACCOUNT_TEMPLATE.aliMessageService = aliMessageService;
CONTRACT_EXPIRE_TEMPLATE.aliMessageService = aliMessageService;
BILL_TEMPLATE.aliMessageService = aliMessageService;
VISIT_OFFICE_TEMPLATE.aliMessageService = aliMessageService;
}
}
}
b. 定义一个发送短信的方法即可
public Boolean send(String phoneNo, Object... objs) {
Object[] no = new Object[objs.length];
if (objs.length > 0) {
for (int i = 0; i < objs.length; i++) {
String str = objs[i].toString();
if (str.length() > 25) {
str = str.substring(0, 22) + "...";
}
no[i] = str;
}
}
SendMessageForm form = new SendMessageForm();
form.setPhoneNo(phoneNo);
form.setParamJson(StringUtils.isNotBlank(this.paramJson) ? String.format(this.paramJson, no) : "");
form.setTemplateCode(this.tempCode);
SendMessageVo sendMessageVo = aliMessageService.sendMessage(form);
log.info("短信发送信息 -> {}", sendMessageVo);
if (!"OK".equals(sendMessageVo.getCode())) {
throw Result.toBizException(ResultCodeEnum.SEND_MESSAGE_FAIL, sendMessageVo.getMessage());
}
return true;
}
c. 使用
五. 总结
以前使用枚举仅仅是作为业务状态的多种模板提供选择的作用,现在可以通过枚举简化代码处理各种服务,代码入侵基本为0。