经验积累

前言

  身为一个程序员,自我总结是很重要的。它能帮助你了解自身所掌握的技术,也能让你知道自己还有哪些不足。本文旨在总结作者从业两年的经验,讲的大多是一些基础性的、通用的开发经验。

体系架构

  很久以前,基于接口的三层架构已经很流行了,到现在,它依然是单机应用的首先架构。Web层是spring的springboot,service层和dao层由接口隔离,也即面向接口编程,由spring提供ioc依赖注入和aop切面拦截。在dao层使用数据访问技术jdbc、jpa、hibernate、mybatis等访问数据库,service层主要关注业务。

通用json数据格式

  现如今最流行的由后端返回给前端的数据格式,完美契合javascript。做为一个后端开发人员,对于返回的数据,实在是不希望数据格式变来变去,所以需要一个标准。示例如下:

public class CmResult {
    private int code;
    private String message;
    private Object data;
    //get set
	//other methods
}

  对于code的设计,参考了http状态码,2开头的代表成功,4开头的代表客户端错误,5开头的代表服务端错误。比如一个简单的系统,code为如下设计。
  20000代表成功,40102:4代表是客户端错误,01代表模块,02代表该模块下的某种错误,不够可延长。除第一位不变以外,0 的个数根据系统大小程度而定(功能模块数和该模块下可能出现的错误数)。通常这个code会写成一个枚举。

public enum ResultCodes {
    SUCCESS(20000, "success"),
    CLIENT_ERROR(40000, "客户端错误"),
    SERVER_ERROR(50000, "服务器错误");
    //region 私有构造 及 属性
    private int code;
    private String message;
    private ResultCodes(int code, String message){
        this.code = code;
        this.message = message;
    }
    public int getCode(){ return this.code; }
    public String getMessage(){ return this.message; }
    //endregion
}

异常处理

  CmResult只在Controller处使用,而ResultCodes是全局的,那么当service出现业务逻辑错误的时候怎么返回这个错误。返回CmResult带个code?不是的,在service业务出现错误的时候,不返回CmResult,只关注正确的返回,所有的错误直接抛异常。这就引出了自定义异常,示例如下:

public class SysException extends RuntimeException {
    private ResultCodes resCode;
    private String extraMessage;
    public SysException(ResultCodes resCode) {
        this.resCode = resCode;
    }
    public SysException(ResultCodes resCode, String extraMessage){
        this(resCode);
        this.extraMessage = extraMessage;
    }
    public ResultCodes getResultCode() { return resCode; }
    public String getMyMessage() {
        return extraMessage == null || extraMessage.isEmpty() ?
                resCode.getMessage() :
                MessageFormat.format("{0} -> {1}", resCode.getMessage(), extraMessage);
    }
}

  自定义异常继承RuntimeException,免去了在方法处声明或强制手动try catch,带有 code 和 message,真实错误由ResultCodes给出,并且整个系统只使用一个自定义异常。
  异常是抛了,由谁处理呢?这时候当然是由全局异常处理器来处理了。捕获异常后,判断是不是自定义异常,是的话返回自定义json数据,带有code和message,非业务错误返回code为50000,意为未知错误,比如空指针啥的。

@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {
    @ExceptionHandler(Exception.class)
    public CmResult allExceptionHandler(Exception exception) {
        if (exception instanceof SysException){
            SysException e = (SysException) exception;
            return CmResult.bad(e.getResultCode(), e.getMyMessage());
        }
        if (exception.getCause() instanceof SysException){
            SysException e = (SysException) exception.getCause();
            return CmResult.bad(e.getResultCode(), e.getMyMessage());
        }
        return CmResult.bad(ResultCodes.SERVER_ERROR, MessageFormat.format("{0} -> {1}", ResultCodes.SERVER_ERROR.getMessage(), exception.getMessage()));
    }
}

  如果是非业务逻辑的错误,比如工具类里,直接抛出异常带个消息,可以不用定义code,只在业务里调用这个工具类时try catch。

各层间的数据传输

  dto、vo、do、query…,各种o,dto用的多,其他就看情况吧,自行斟酌。

client -> controller:dto、query
controller -> service:dto、query
service -> dao:query、do
dao -> service:do、dto
service -> controller:dto
controller -> client:dto、vo

日志记录log

  日志是个重要的东西,如果一个系统没有记录日志,将来查错都费事。对于通用的日志,即流水线日志,就是将一个请求从头到尾经过的地方log起来(如果有需要的话)。可以建个切面,拦截controller、service、dao、工具类,前置log参数值,后置log返回值,异常log异常消息,一个环绕通知就可搞定。在请求来的时候,给个uuid标识一下,log的时候根据uuid标识各个请求的路线。对于需要更细致的日志,可以在类的方法里自行log。通常一些重要的日志也会记录进数据库里,方便查阅,比如用户操作日志。

用户状态

  登录是每个程序员最先经历的一道坎,因为涉及用户状态的保存问题,这要分情况讨论。
  如果是单机web程序,不用想,直接cookie和session就完事了,前人早已为我们种好树了。
  如果不只是web,还有app等移动端,cookie就不好用了,怎么办?其实cookie只是保存了sessionid而已,把sessionid返回给移动端不就完事了么。
  是的,通常有其他端要使用的话,会将session(会话)保存在redis里,使用uuid当key,value存用户信息和其他信息(hash数据结构),这个uuid就是sessionid,返回给移动端。使用redis保存会话的好处就是方便session共享,因为传统的session只在一台机器上保存,如果是集群就不能在其他机器上使用了,虽然也可以复制session到其他机器上,但是单台机器的内存总是有限的,不如redis扩展性强,用redis会方便些。
  有人会问了,uuid不能一直有效吧,没事,redis可以设置过期时间。
  那uuid过期了,得重新登录?不好吧。那行,咱可以延长uuid的有效期。
  那我一直用,一直延长,不就无限期了?那咱可以定时换uuid,在第一次登录时,签发一个uuid,有效期为60分钟,更换时间定为15分钟,请求时检查uuid的时间(redis有记录签发时间)和当前时间对比,在15分钟内就正常请求,如果超过15分钟,那么签发一个新的uuid,将原uuid的值保存在新的uuid里,并设置原uuid的过期时间为1分钟的缓冲时间,防止并发,而新uuid的过期时间还是60分钟。
  如果在过期时间并发访问,两个uuid都要去换新的uuid咋办?这时候redis uuid的value里应该记录一个值:是否更换过新的uuid,换之前要判断,如果线程一去换过了,要设置一个标记,其他线程就不允许再换,保证只有一个线程能更新uuid,这里要注意的就是redis的操作要保持原子性,判断、设标记、set新的key、设置过期时间,原子性操作很重要。
  还有人要问如果uuid被盗用了怎么办?咱可以加个ip,判断请求的ip是不是和第一次签发时记录的ip一致,但如果是局域网里的就很尴尬了,因为获取不到真实的ip,也没办法通过请求获取用户机器的唯一标识,这是个无解的问题,我是没想出来。
  redis uuid session的方法基本能解决问题了,关于uuid被拦截的问题,还是把https上了再说这些吧,把能上的都上了,还能丢的话,那谁也没办法。就像你家钥匙丢了,你能怎么办,想安全还不是得换锁。
  以上说的都是有状态的服务,还有一种完全无状态的方法,就是token,典型的标准就是jwt,将用户信息存在token里,系统不保存任何状态。其实也是个不错的东西,但因为jwt是base64编码,信息是能看的,所以我更喜欢自己实现,将用户信息:id、username、timestamp,通过私有key签名,防止篡改,再整个加密一下,基本算实现了简单的自定义token,不过这依然解决不了token被盗的问题。

权限

  权限是比较简单的了,基本操作是先拦截,再判断是否放行,拦截的话用过滤器、拦截器、切面都ok。以filter为例,如果是粗粒度的,整个页面算一个权限,那直接在doFilter里判断用户是否有权限访问该地址了。
  如果是细粒度的,设计上就是user、role一对多,role、permission一对多,user、permission一对多(特权),总的权限就是角色权限和特权的并集。老样子,在action方法上标记权限码,过滤器里查用户权限码,判断即可,权限通常很少变,可缓存。

全局用户信息

  请求过来时,在拦截器拦截,获取uuid,即sessionid,从redis里查用户信息,然后存到threadlocal里,一个静态帮助类,方便在各个地方获取当前用户信息。如果是token型的,直接解密、验证签名,就可以将token里的用户信息存在threadlocal里。

程序代码的接口调用

  有时候,不同的系统之间需要进行通信,这时通常会写个接口,由另一个系统在方法里发请求调用。如何确保接口的安全性?如果系统发布在公网,总要保证接口不会被乱调,要信得过的才能使用。
  这时模仿一下微信的做法,先申请一个appid和appkey,将请求参数进行排序(包括一个时间戳和appid)拼接,最后拼接上appkey,进行sha256签名,将签名结果sign一同发过去。服务端收到请求后,判断时间戳与当前时间是否超过5分钟,超过则拒绝,为保证请求的唯一性,将签名sign存在redis中,过期时间5分钟,每次先查redis是否有相同的sign,有则拒绝请求,没有的话再用相同的规则计算签名,与传来的签名比较,一致才放行,并缓存sign。

设计模式

  这个很重要,学会设计模式后,代码质量会有质的提升,这里不多说,网上资料很多。

领域建模

  之前看了一些教程,但是自己用的不好,因为用着用着就变成和数据表对应起来了。后期有时间还是应该学习并应用起来。

数据库

  数据库的设计主要有两方面,一是表的设计、二是索引的设计,通常后者都会被忽略,因为有主键索引了。表的设计与业务相关,不多说。索引对查询速度的影响最大,是sql优化里作用最明显的一种。这里只是抛个砖,还是专业书籍来的详细。

小结

  作为一名优秀的CRUD工程师,我自以为掌握了三层,会了点设计,代码写得好看了一点,就可以行走江湖了。奈何江湖水太深,我这井底之蛙,还稍欠火候。
  自从看了分布式、微服务、集群、数据库主从复制读写分离、redis主从复制读写分离之后,才发现自己会的太少,本以为达到了巅峰,却没想到是另一个境界的开始。不管说什么都无法掩饰我的菜,庆幸能够意识到不足,有了目标,有了方向,就能够为之去奋斗,一点一滴地攻破、掌握这些技术。

参考文章:https://juejin.im/post/5b83466b6fb9a019b421cecc
该文章介绍了分布式的一些概念,还有一套springcloud的例子,挺不错。

参考文章:https://www.cnblogs.com/xinhuaxuan/category/963844.html
这篇是redis的教程,数据结构、基本操作、持久化、复制、集群都有,真是让我受益匪浅。

虽然理论看了很多,但实践才是检验真理的唯一标准。只有动手做了,理解了,才是自己的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值