引言
最近花了大概20天来完成了一个单体的前后端分离的项目,从8.25开始,到现在,也是挺心累的。整体的代码没致命的问题,但是也有些小bug没有解决。总体来说也算是一个小阶段性成果吧。
数据库建设
1 spring报错sql语法异常
确保表名、数据库名、字段名都不使用关键词
数据库名,表的表名,字段名都不允许使用mysql相关的关键词,否则会报sql语法异常的错误!
这个坑导致我前前后后找了3个小时,原因是把一个表名设成了order
,我们都知道,order
是mysql里面的关键字,因此当Mysql处理sql语句时把这个order
当作order by
来处理了!
具体的mysql关键字在这个网址里:mysql关键词查询
2 字段名数量
首先在设计整个项目的数据库时就要考虑清楚,哪些字段名是传参时候就必须的,哪些是使用DTO来扩展的,哪些是使用公共字段自动填充的,哪些是直接丢在Redis里面的或者在mongodb里面的,否则,当你发现前端传回来的值有多余或者缺少时是件非常麻烦的事情。
DTO解决简化了多次数据传输的冗余以及简化了数据库
3 公共字段自动处理
最新操作时间、最新操作人这类所有操作都会触发的字段,可以封装成一个全局公共字段自动处理类来使用(没错,就是aop),简化了业务层的开发
第三方中间件缓存的坑
当前端传回一个值时,我们在后端需要有相应的地址来接收,并且处理相应的参数值缓存到第三方中间件内(比如redis),而前端传回的值参差不齐,如果没有统一的前后端交互协议以及结果处理,那么很有可能导致后期代码扩展性差
我们需要一个共有的字段来处理同一个缓存结果,否则其它的更新/删除操作而产生的数据的更新将不及时(会按照cache的储存时长TTL来更新)
这个共有字段的问题困扰了我一个晚上,最终因前端页面写死,无法大刀阔斧地改动而罢休
通用处理类
1 线程副本
可以使用ThrealLocal线程副本来保存登录信息
但是这种方法在分布式中无法使用
2 自定义业务异常类处理
在全局异常里面使用
这样的好处是可以捕获运行时的异常进行相应的处理
3 Json序列化器
这个序列化器是用于保证json正确地将值在spring框架中传送的
/**
* 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
* 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
* 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]
*/
public class JacksonObjectMapper extends ObjectMapper {
public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";
public JacksonObjectMapper() {
super();
//收到未知属性时不报异常
this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);
//反序列化时,属性不存在的兼容处理
this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
SimpleModule simpleModule = new SimpleModule()
.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))
.addSerializer(BigInteger.class, ToStringSerializer.instance)
.addSerializer(Long.class, ToStringSerializer.instance)
.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
//注册功能模块 例如,可以添加自定义序列化器和反序列化器
this.registerModule(simpleModule);
}
}
4 前后端协议联调
设置一个R结果处理集,在R内的是T任意类型,以保证其通用性
@Data
public class R<T> {
/**
* 状态码:1成功,0失败
*/
private Integer code;
/**
* 错误信息
*/
private String msg;
/**
* 数据
*/
private T data;
/**
* 动态数据
*/
private Map map = new HashMap();
/**
* 响应成功状态
*
* @param object
* @param <T>
* @return r
*/
public static <T> R<T> success(T object) {
R<T> r = new R<T>();
r.data = object;
r.code = 1;
return r;
}
/**
* 响应失败状态
*
* @param msg
* @param <T>
* @return r
*/
public static <T> R<T> error(String msg) {
R<T> r = new R<T>();
r.msg = msg;
r.code = 0;
return r;
}
/**
* 增加动态数据
* @param key
* @param value
* @return R
*/
public R<T> add(String key, Object value) {
this.map.put(key, value);
return this;
}
}
通用配置类
1 mybatisplus分页配置类
若要使插件生效,必须加上配置类
@Configuration
public class MybatisPlusConfig {
/**
* 分页查询插件的配置类
*
* @return
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mybatisPlusInterceptor;
}
}
2 redis序列化配置类
为保证redis的key名能够被接收并调用,必须对key进行序列化,否则会出现/0x/6x
之类的代码(value不需要,当然,加上也没关系)
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(connectionFactory);
return redisTemplate;
}
}
3 前端JS精度丢失问题
由于我们的数据库id是雪花算法生成的,因此使用Intenger已经不再能接收值(Long才可以),对于JS来说,更为致命(由于数字过长,JS会将后两位处理为0,即精度丢失)
@Slf4j
@Configuration
//springboot建这个项目的写这个mvc配置类要实现WebMvcConfigurer接口,而不是继承WebMvcConfigurationSupport
public class WebMvcConfig implements WebMvcConfigurer {
/**
* 扩展mvc框架的消息转换器,可以兼容前端js与后端java的数据类型,处理精度丢失等问题
*
* @param converters
*/
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("创建转换器");
//创建消息转换器对象
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
//设置对象转换器,底层使用jackson将java对象转为json
messageConverter.setObjectMapper(new JacksonObjectMapper());
//将上面的消息转换器对象追加到mvc框架的转换器内
converters.add(0, messageConverter);
}
}
4 拦截器
拦截器可以有多个,这里因为代码是在本地跑的,就只需要一个了
@WebFilter(filterName = "loginCheckFilter", urlPatterns = "/*")
@Slf4j
public class LoginCheckFilter implements Filter {
//路径匹配器,支持通配符
public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
//1.获取本次请求的uri
String requestURI = request.getRequestURI();
log.info("拦截到请求: {}", requestURI);
//2.定义不需要处理的请求路径
String[] urls = new String[]{
"/employee/login",
"/employee/logout",
"/backend/**",
"/favicon.ico",
"front/**",
"common/**",
"/user/**"
};
//3.判断本次请求是否需要处理
boolean check = check(urls, requestURI);
//请求合法,不需要处理
if (check) {
log.info("放行{}", requestURI);
filterChain.doFilter(request, response);
return;
}
//后台:请求不合法,需要处理,判断用户是否登录,如果登录,那就放行;如果未登录,那就通过输出流的方式向客户端页面响应数据
if (request.getSession().getAttribute("employee") != null) {
log.info("用户已登录,id为{}", request.getSession().getAttribute("employee"));
Long empId = (Long) request.getSession().getAttribute("employee");
BaseContext.setCurrentId(empId);
filterChain.doFilter(request, response);
return;
}
//前台:请求不合法,需要处理,判断用户是否登录,如果登录,那就放行;如果未登录,那就通过输出流的方式向客户端页面响应数据
if (request.getSession().getAttribute("user") != null) {
log.info("用户已登录,id为{}", request.getSession().getAttribute("user"));
Long userId = (Long) request.getSession().getAttribute("user");
BaseContext.setCurrentId(userId);
filterChain.doFilter(request, response);
return;
}
log.info("用户未登录");
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
return;
}
/**
* 路径匹配,检查本次请求是否需要放行,匹配的上与否
*
* @param urls
* @param requestURI
* @return boolean
*/
public boolean check(String[] urls, String requestURI) {
for (String url : urls) {
boolean match = PATH_MATCHER.match(url, requestURI);
if (match) {
return true;
}
}
return false;
}
}
其它
1 实体类
实体类必须序列化,以保证数据的安全性implements Serializable
再自定义一个UIDprivate static final long serialVersionUID = 1L;
如果遇到公共字段,需要使用@TableField
注解来使公共类自动装配生效
2 工具类
工具类也要注意需要加上@Component
否则无法被spring给装配到core容器内
3 事务
如果业务需要调用事务的,那么需要在业务层加上事务注解
并且在启动项里开启事务