最近在做SpringBoot+Vue微信点餐项目,虽说是老项目,有很多大牛已经玩烂了或者是觉得小儿科的东西不屑于去玩,但是本人还处于学习阶段,想要利用业余时间把项目从框架搭建+业务逻辑+项目部署自己完成,同时前端也正在使用Vue.js编写,当然该项目也不局限于原版,也会在业务方面会按照自己的想法做(暂时想的创业方向),现在就把目前碰到的一些springboot2.1.7+vue-cli2的问题和大家说一说~这篇文章会慢慢补充更新和完善,包括后续在服务器部署的问题上,我也会和大家说一说,这也属于是本人第一个个人项目,希望对加入程序猿行列的新手有所帮助(虽然自己也是新手),也希望大家能够一起探讨,互相学习,共同进步!~
一、先附上gitHub地址,以便大家查(帮!)看(忙!)代(点!)码(星!)~(*)~(*)~
后端项目地址:https://github.com/yiboyuntian97/WOS-admin.git
前端项目地址:https://github.com/yiboyuntian97/WOS-front.git
二、项目目录结构
三、问题
项目所需环境就不详细说了,相信大家对jdk环境变量和node环境变量这些都不陌生,那我们就从后端遇到的问题开始说起。
A.数据源配置及使用,也是本篇文章起初的重中之重
1.application.yml中对mysql5.7的数据源配置
其中第二项如果不更换,会报出Loading class `com.mysql.jdbc.Driver'. This is deprecated错误
其中第三项如果不配置,会报出The server time zone value '�й���ʱ��' is unrecogni。。。。错误
2.说完了配置文件,我们来说DataSorceConfig
也许在配置完数据源后,你启动项目会报出需要entityManger这个bean,那么此时就需要我们手动写一个bean实体,并添加到spring容器中
a.DataSourceConfig类
b.PrimaryConfig类,直接上代码
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
entityManagerFactoryRef = "entityManagerFactoryPrimary",
transactionManagerRef = "transactionManagerPrimary",
basePackages = {"com.wangyb.sell.repository"}) //设置Repository所在位置
public class PrimaryConfig {
@Resource
private JpaProperties jpaProperties;
@Autowired(required = false)
private HibernateProperties hibernateProperties;
@Autowired
@Qualifier("primaryDataSource")
private DataSource primaryDataSource;
@Primary
@Bean(name = "entityManagerPrimary")
public EntityManager entityManager(EntityManagerFactoryBuilder builder) {
return entityManagerFactoryPrimary(builder).getObject().createEntityManager();
}
@Primary
@Bean(name = "entityManagerFactoryPrimary")
public LocalContainerEntityManagerFactoryBean entityManagerFactoryPrimary(EntityManagerFactoryBuilder builder) {
Map<String,Object> properties = hibernateProperties.determineHibernateProperties(jpaProperties.getProperties(),new HibernateSettings());
return builder
.dataSource(primaryDataSource)
.properties(properties)
.packages("com.wangyb.sell.dataObject") //设置实体类所在位置
.persistenceUnit("primaryPersistenceUnit")
.build();
}
// private Map getVendorProperties(DataSource dataSource) {
// return jpaProperties.getHibernateProperties(dataSource);
// }
@Primary
@Bean(name = "transactionManagerPrimary")
public PlatformTransactionManager transactionManagerPrimary(EntityManagerFactoryBuilder builder) {
return new JpaTransactionManager(entityManagerFactoryPrimary(builder).getObject());
}
}
重点说明:
(1)注释的代码段,在springboot1.0版本是可用的,但在2.0中getHibernateProperties()这个方法已经被开发者修改为不带参数,此处要注意
(2)其中的JpaPropertites如果用@Autowired来装配则会出现‘Could not autowire. No beans of 'xxx' type found’类似错误,所以暂换成@Resource来装配,原因后续慢慢调查...
3.在使用repository仓库时,findOne()会报出‘ No property findOne found for type ProductCategory!’这个错误,只需把项目中用到的findOne()方法换成findBy(PrimaryKey)Id即可,原因目前暂不清楚,如果有清楚的小伙伴,麻烦不吝告之,谢谢!~
文章开始时间2019.8.13,目前前端项目暂未出现什么问题,后续会慢慢更新之~
刚开始写文章,有很多不足之处,如有好的建议,请各路大神多多指教,笔芯~^
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~分割线~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
文章更新~~2019.8.26
时隔半个月,该项目已经敲到尾声了,期间业务代码敲起来比较顺畅,在代码中有几个好的建议可以和大家一起分享一下,这里大部分说明直接用代码举例啦~~
一、关于接收前端传参验证上,springboot帮我们在框架层面做了很好的集成
eg:
/**
* 买家姓名
*/
@NotEmpty(message = "姓名必填")
@Max(value = 10)
private String name;
/**
* 买家手机号
*/
@NotEmpty(message = "手机号必填")
@Pattern(regexp = "1[3|4|5|7|8][0-9]\\d{8}")
private String phone;
/**
* 买家地址
*/
@NotEmpty(message = "地址必填")
private String address;
其中@NotBlank,@NotEmpty是对非空字段的校验,这两者的区别前者包含了空、null和空字符串“ ”的校验,更加严格;@Max@Min对长度的校验,参数value对应的是该字段长度大小;@Pattern是对字符串格式的校验,比如手机号、邮箱等,参数regxp对应的是正则表达式,你也可以使用@Email,@Phone自定义注解;@Post 限定一个日期,日期必须是过去的日期;@Future 限定一个日期,日期必须是未来的日期;除了这些常用注解外当然还有其他注解等待我们在业务中不断挖掘......
当我们在VO层定义了这些注解后,Controller层该如何使用呢,话不多说,直接上代码
@PostMapping("/create")
public ResultVO<Map<String,String>> create(@Valid OrderForm orderForm, BindingResult bindingResult) {
if(bindingResult.hasErrors()) {
log.error("【创建订单】参数不正确,orderForm={}",orderForm);
throw new SellException(ResultEnum.PARAM_ERROR.getCode(),bindingResult.getFieldError().getDefaultMessage());
}
OrderDTO orderDTO = OrderForm2OrderDTOConverter.convert(orderForm);
if(CollectionUtils.isEmpty(orderDTO.getOrderDetailList())) {
log.error("【创建订单】错误,购物车为空");
throw new SellException(ResultEnum.CART_EMPTY);
}
OrderDTO createResult = orderService.create(orderDTO);
Map<String,String> map = new HashMap<>();
map.put("orderId",createResult.getOrderId());
return ResultVOUtil.success(map);
}
我们可以看到,在接口参数中,有两个对象,第一个便是我们刚定义好的表单验证数据对象,当然你也可以直接使用VO这个bean,第二个参数为BindingResult,该对象帮我们接收前者的校验信息,其中hasErrors()方法判断是否含有校验未通过字段,getFieldError().getDeafultMessage()方法直接帮我们拿到我们在bean中指定好的校验信息,这里需要注意的是@Valid 和 BindingResult 是一 一对应的,如果有多个@Valid,那么每个@Valid后面都需要添加BindingResult用于接收bean中的校验信息!
二、我们说几个项目中常用的工具类
1.封装Controller层返回工具类ResultVOUtil
在前后端分类项目中,后端更多的专注于接口的开发,在前后端约定的接口文档上,后端大部分给前端返回的都是基于resutful风格的json格式,前端拿到json后可以自己进行数据的处理和渲染,关注点在返回的code,message和data数据上,其中data可能是一个字符串,可能是一个列表,我们可以把返回给前端的对象这么定义,这其中利用了泛型,大多数小伙伴只是用了公司架构师给定义好了的返回对象,我们今天来剖析一下其中的实质~
@Data
public class ResultVO<T> {
/**
* 错误码
*/
private Integer code;
/**
* 提示信息
*/
private String message;
/**
* 具体内容
*/
private T data;
}
接下来有了这个返回对象,难道我们每次controller中都要new一个,然后去set吗?java的方便之处就是他会鼓励我们去封装,但是也是他不好的一点是,有时候我们看不到底层,只停留在应用层面,这里言归正传,直接上返回对象工具类
public class ResultVOUtil {
public static ResultVO success(Object object) {
ResultVO resultVO = new ResultVO();
resultVO.setData(object);
resultVO.setCode(0);
resultVO.setMessage("成功");
return resultVO;
}
public static ResultVO success(){
ResultVO resultVO = new ResultVO();
resultVO.setData(null);
resultVO.setCode(0);
resultVO.setMessage("成功");
return resultVO;
}
public static ResultVO error(Integer code,String message) {
ResultVO resultVO = new ResultVO();
resultVO.setMessage(message);
resultVO.setCode(code);
return resultVO;
}
}
可以看到,只要有java基础的小伙伴们可以自己轻轻松松完成对一个工具类的封装,无非就是利用static这个关键字喽~
二、时间格式工具类Date2LongSerializer
这个没什么好说的,就是把返回给前端的时间戳格式毫秒转秒转换一下,写法也是固定的
public class Date2LongSerializer extends JsonSerializer<Date> {
@Override
public void serialize(Date date, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeNumber(date.getTime() / 1000);
}
}
在使用时我们需要在对应时间字段上加上JsonSerialize 这个注解,并使用上我们刚刚定义好的类
@JsonSerialize(using = Date2LongSerializer.class)
private Date createTime;
这里直接说一下,在开发中,我们可以将null的字段在返回给前端时自动过滤掉,在引入了谷歌的gson依赖后,在application.yml可以进行如下配置,同时在返回VO中加上@JsonInclude(Inclue.NON_NULL)这个注解即可
jackson:
default-property-inclusion: non_null
三、异常处理+异常捕获
1.异常处理,自定义异常类,这里的构造方法入参可以使用枚举类
public class SellException extends RuntimeException {
private Integer code;
public SellException(ResultEnum resultEnum) {
super(resultEnum.getMessage());
this.code = resultEnum.getCode();
}
public SellException(Integer code,String message) {
super(message);
this.code = code;
}
}
public enum ResultEnum {
SUCCESS(0,"成功"),
;
private Integer code;
private String message;
ResultEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
public Integer getCode() {
return code;
}
public String getMessage() {
return message;
}
}
2.异常捕获 自定义一个handler.SellerExceptionHandler类
@ControllerAdvice
public class SellerExceptionHandler {
//拦截登录异常
@ExceptionHandler(SellerAuthorizeException.class)
public ModelAndView handlerAuthorizeException(){
return new ModelAndView("redirect:+loginurl")
}
}
这里异常捕获以下文提到的登录拦截举例
四、登录登出、分布式session
这里的基本原理就是拿到登录人的用户名密码后验证后设置token至redis,然后设置token至cookie
五、aop身份验证、登录拦截
Aspect
@Component
public class SellerAuthorizeAsoect {
/**
* 切入点
*/
@Pointcut(value = "execution(public * com.***.controller.Seller*.*(..)) + &&!execution(public * com.***.controller.SellerUserController.*(..)))
public void verify() {}
/**
* 切入点之前执行
*/
@Before("verify()")
public void doVerify(){
ServletRequestAttributes attributes = RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 查询cookie中token,查询redis中token
}
}
六、redis分布式锁
1.redis的setnx命令:将key设置值value,如果key不存在,等同于set命令,当key存在时,什么也不做,可以使用!setnx加锁
2.redis的getset命令:自动将key对应到value并且返回原来的key对应的value,如果key存在,但对应的value不是字符串,就返回错误,先get然后set
3.代码实现
@Component
@Slf4j
public class RedisLock {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 加锁
* @param key
* @param value 当前时间+超时时间
* @return
*/
public boolean lock(String key,String value) {
if(redisTemplate.opsForValue().setIfAbsent(key,value)){
return true;
}
String currentValue = redisTemplate.opsForValue().get(key);
//如果锁过期
if(!StringUtils.isEmpty(currentValue) && Long.parseLong(currentValue )< System.currentTimeMillis()){
//获取上一个锁的时间
String oldValue = redisTemplate.opsForValue().getAndSet(key,value);
if(!StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue)) {
return true;
}
}
return false;
}
/**
* 解锁
* @param key
* @param value
*/
public void unlock(String key,String value) {
String currentValue = redisTemplate.opsForVlaue().get(key);
if(!StringUtils.isEmpty(currentValue)&¤tValue.equals(value)) {
redisTemplate.opsForValue().getOperations().delete(key);
}
}
}
七、利用@Cacheable\@CacheEvict\@CachePut进行redis缓存
八、freemarker模板的几点使用说明
1.对于现在前后端分离为主流来说,模板语言使用的并不多,其中thymeleaf这个模板比freemarker更适用于前后端分离,这里也不深入阐述了,介绍一些使用技巧,如果大家想深入研究可以到官网学习,同时有什么好的建议也欢迎大家评论留言~
2.在freemarker使用中,是以.ftl为后缀名的文件,为了快速搭建管理系统,我们可以使用bootstrap和jquery
3.在idea中,如果仅修改了.ftl,可以ctrl+f9重新编译前端模板,不用重启项目,有助于我们快速调试
九、整合websocket
1.引入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>
2.写配置类
@Component
@ServerEndpoint("/webSocket")
@Slf4j
public class WebSocket {
private Session session;
private static CopyOnWriteArraySet<WebSocket> webSockets = new CopyOnWriteArraySet<>();
@OnOpen
public void onOpen(Session session) {
this.session = session;
webSockets.add(this);
log.info("【webSocket消息】有新的连接,总数:{}",webSockets.size());
}
@OnClose
public void onClose() {
webSockets.remove(this);
log.info("【websocket消息】连接断开,总数:{}",webSockets.size());
}
@OnMessage
public void onMessage(String message) {
log.info("【websocket消息】收到客户端发来的消息:{}",message);
}
public void sendMessage(String message){
for(WebSocket webSocket :webSockets) {
log.info("【websocket消息】广播消息,message{}",message);
try {
webSocket.session.getBasicRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
十、项目部署
1.在原始单体架构的部署上,我们使用的方法是安装Tomcat服务器,然后把我们打好的war包方到目录下面,然后启动就可以了,这个方法有一定的局限性,比如说,想要部署两个应用程序,分别使用不同端口,我们需要在Tomcat中手动配置,或者程序区分多个环境,和水平垂直扩展,tomcat也不好配置
2.由于springboot帮我们内嵌了tomcat服务器,所以目前这里我们的项目使用java -jar 打成jar包部署即可。
首先使用mvn clean package (-Dmaven.test.skip=true,括号中帮我们做单元测试,可以略掉)打包
BUILD SUCCESS 后,包放到了target目录下,pom文件中,filename可以帮我们修改好jar包的名字,如下
<build>
<finalName>sell</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
可以scp target/sell.jar root 192.168.0.0(ip):/opt/javaapps 上传到服务器/opt/javaapps目录下
在服务器对应目录下 java-jar sell.jar即可启动~
3.修改端口命令:java -jar -Dserver.port=8090 sell.jar
4. 区分环境启动:java -jar -Dserver.port=8090 -Dspring.profiles.active=prod sell.jar
5.后台启动:nohup java-jar sell.jar > /dev/null 2>&1 &
这时可以使用ps -ef|grep sell.jar查看进程,看是否已启动
6.脚本启动:vim start.sh
#!/bin/sh
nohup java-jar sell.jar > /dev/null 2>&1 &
保存后,bash start.sh
7.centos7推荐service启动
cd /etc/systemd/system/ 目录下 新建sell.service文件
vim sell.service 脚本如下
[Unit]
Description=sell 描述
After=syslog.target network.target 依赖
[Service]
Type=simple 模式
ExecStart=/usr/bin/java -jar /opt/javaapps/sell.jar
ExecStop-/bin/kill -15 $MINPID
User=root
Group=root
[Install]
WanteBy=multi-user.target
保存后 systemctl daemon-reload systemctl start sell.service 项目启动
systemctl stop sell 项目停止 systemctl enable sell 开机启动 systemctl disable sell 停止开机启动
十、下阶段规划展望
这篇博客到这里主要介绍了一些springboot的问题,还没有介绍vue,接下来我会去完善vue,并分享给大家,在vue实现之后,这里主要写写我现在个人的想法,在目前主流的分布式上,代码使用springcloud框架,说到springcloud,我们会学习到服务注册与发现的Eureka注册中心、服务配置网关的Zuul、服务通信调用负载均衡的Ribbon、分布式配置的Spring Cloud Config、服务治理方面的断路器Hystrix、以及消息总线Spring Cloud Bus,在架构上我们利用的是nginx做负载均衡,分发请求到Tomcat集群上,然后redis高可用缓存集群,数据库持久层利用mysql分库分表,同时还要兼顾RocketMQ或者Kafka做消息队列去异步消息和应用解耦、在部署上利用docker+k8s+git+jenkins实现容器化部署,这些技术需要我去慢慢学习和掌握,同时也进行项目实战,如果有对这个技术感兴趣的小伙伴想共同学习的话可以持续关注我的博客,大家一起开车,驶向幼儿园,不,驶向架构的深渊~~谢谢大家的观看~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~分割线~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~