身为一个程序员,自我总结是很重要的。它能帮助你了解自身所掌握的技术,也能让你知道自己还有哪些不足。本文旨在总结作者从业两年的经验,讲的大多是一些基础性的、通用的开发经验。
很久以前,基于接口的三层架构已经很流行了,到现在,它依然是单机应用的首先架构。Web层是spring的springboot,service层和dao层由接口隔离,也即面向接口编程,由spring提供ioc依赖注入和aop切面拦截。在dao层使用数据访问技术jdbc、jpa、hibernate、mybatis等访问数据库,service层主要关注业务。
现如今最流行的由后端返回给前端的数据格式,完美契合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起来(如果有需要的话)。可以建个切面,拦截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的教程,数据结构、基本操作、持久化、复制、集群都有,真是让我受益匪浅。
虽然理论看了很多,但实践才是检验真理的唯一标准。只有动手做了,理解了,才是自己的。