不少的同学在正式找工作前肯定都接触过了Java著名的SSM框架,当然SpringBoot和SpringCloud相信很多人也都了解过,前一段时间我抽空完完整整的完成了一个基于这些主流技术的“项目”,并且体验了一把将项目部署到云服务器上,从此调试不再是localhost了而是我自己的域名,对于现阶段来讲确实是一件很酷的事情,幸运的是在完成的过程中我完整的保留了每一步开发的笔记,现做整理如下,希望可以帮助到需要的同学:
高并发系统的演进应该是循序渐进,以解决系统中存在的问题为目的和驱动力的,我们需要先从最基础的单体项目开始搭建。
友情提示:一入此坑深似海,没有几个星期的功夫是肯定没办法完成的,我的文章也会分成好几篇来写,前方高能,请想好了再上车!!
老规矩,源码请联系公众号:
本文只提供后端代码的详细编写流程,前端源码不是我们要关注的重点,文内只会给出片段,想要页面展示效果的也请联系公众号,仅仅使用PostMan测接口的话不需要前端源码。
接口文档api:http://wjwqxy.cn:8088/foodie-dev-api/doc.html
同样的,数据库设计的也比较简单,可以自己动手或者找我要~
话不多说,下面正式开始。
SpringMVC 对比 SpringBoot:
前端技术选型:
采用前后端分离的开发模式:
浏览器请求的是反向代理服务器nginx中的静态页面,nginx可以通过Restful访问另外一台服务器
使用Maven项目管理工具,项目名称为foodie-dev
,拆分出通用模块common
,类模块pojo
,映射器模块mapper
,服务模块service
和控制器模块controller
等。模块之间使用pom文件关联起来。
创建foodie-dev-common
模块、foodie-dev-pojo
模块。
foodie-dev-pojo
依赖foodie-dev-common
<dependency>
<groupId>com.wjwgroupId>
<artifactId>foodie-dev-commonartifactId>
<version>1.0-SNAPSHOTversion>
dependency>
创建foodie-dev-mapper
模块,依赖pojo
<dependency>
<groupId>com.wjwgroupId>
<artifactId>foodie-dev-pojoartifactId>
<version>1.0-SNAPSHOTversion>
dependency>
创建foodie-dev-service
模块,依赖mapper
<dependency>
<groupId>com.wjwgroupId>
<artifactId>foodie-dev-mapperartifactId>
<version>1.0-SNAPSHOTversion>
dependency>
创建foodie-dev-api
模块(控制层),依赖service
<dependency>
<groupId>com.wjwgroupId>
<artifactId>foodie-dev-serviceartifactId>
<version>1.0-SNAPSHOTversion>
dependency>
但实际上api不应该调用mapper
所有的创建完毕之后要执行foodie-dev的Maven的安装命令。
在父工程foodie-dev的pom文件中修改
1. 引入依赖 parent
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.1.5.RELEASEversion>
<relativePath />
parent>
之后子模块就可以不用单独制定版本号了。
2. 设置资源属性
<properties>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
<java.version>1.8java.version>
properties>
3. 引入依赖 dependency
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starterartifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-loggingartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
<optional>trueoptional>
dependency>
dependencies>
创建foodie-dev-api的配置文件application.yml
和启动类:
package com.wjw;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 2 * @Author: 小王同学
* 3 * @Date: 2020/12/20 15:11
* 4
*/
@SpringBootApplication
@MapperScan(basePackages = "com.wjw.mapper")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
创建controller包下的类HelloController:
使用@RestController
注解使得返回的对象为json格式
package com.wjw.controller;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import springfox.documentation.annotations.ApiIgnore;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
/**
* 2 * @Author: 小王同学
* 3 * @Date: 2020/12/20 16:29
* 4
*/
@RestController
@ApiIgnore
public class HelloController {
@GetMapping("/hello")
@Transactional
public Object hello() {
return "Hello World";
}
}
为什么SpringBoot可以近乎0配置?
默认的设置来自于@SpringBootApplication
注解,点进该注解中:
我的启动类是在com.wjw包下,当主函数运行后,会自动扫描包下的所有类
@SpringBootConfiguration
注解是一个接口被@Configuration
修饰,表示它是一个容器:
@EnableAutoConfiguration
是开启自动装配的,@Import
是用于做导入的,并且导入的是一个个的Configuration(即容器)
AutoConfigurationImportSelector
是一个选择器,类比jquery,可以选择自动装配的类
getAutoConfigurationEntry
用于自动装配
configurations
被存在于一个List
,来自getCandidateConfigurations
方法
在spring.factories
文件中存了一些自动装配的类
中可以看到内置的tomcat
中内置的是SpringMVC等等等等
导入数据库文件…(略)
HikariCP文档
1. 父工程foodie-dev的pom中引入数据源驱动与mybatis依赖
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.41version>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.1.0version>
dependency>
2. 在doofie-dev-api项目的yml中配置数据源和mybatis
最大连接数经验上是核数的1.5倍
############################################################
#
# 配置数据源信息
#
############################################################
spring:
datasource: # 数据源的相关配置
type: com.zaxxer.hikari.HikariDataSource # 数据源类型:HikariCP
driver-class-name: com.mysql.jdbc.Driver # mysql驱动
url: jdbc:mysql://localhost:3306/foodie-shop-dev?useUnicode=true&characterEncoding=UTF-8&autoReconnect
username: root
password: root
hikari:
connection-timeout: 30000 # 等待连接池分配连接的最大时长(毫秒),超过这个时长还没可用的连接则发生SQ
minimum-idle: 5 # 最小连接数
maximum-pool-size: 20 # 最大连接数
auto-commit: true # 自动提交
idle-timeout: 600000 # 连接超时的最大时长(毫秒),超时则被释放(retired),默认:10分钟
pool-name: DateSourceHikariCP # 连接池名字
max-lifetime: 1800000 # 连接的生命时长(毫秒),超时而且没被使用则被释放(retired),默认:30分钟
connection-test-query: SELECT 1
############################################################
#
# mybatis 配置
#
############################################################
mybatis:
type-aliases-package: com.wjw.pojo # 所有POJO类所在包路径
mapper-locations: classpath:mapper/*.xml # mapper映射文件
同时,在foodie-dev-mapper中创建com.wjw.mapper包,resources中创建mapper包
在foodie-dev-pojo中创建com.wjw.pojo包
3. 内置tomcat
############################################################
#
# web访问端口号 约定:8088
#
############################################################
server:
port: 8088
tomcat:
uri-encoding: UTF-8
max-http-header-size: 80KB
打开我提供的另外一个项目,代码自取
运行utils包下的GeneratorDisplay文件,按照注释做填空题,会根据配置文件来生成mapper和pojo:
<generatorConfiguration>
<context id="MysqlContext" targetRuntime="MyBatis3Simple" defaultModelType="flat">
<property name="beginningDelimiter" value="`"/>
<property name="endingDelimiter" value="`"/>
<plugin type="tk.mybatis.mapper.generator.MapperPlugin">
<property name="mappers" value="com.wjw.my.mapper.MyMapper"/>
plugin>
<jdbcConnection driverClass="com.mysql.jdbc.Driver"
connectionURL="jdbc:mysql://localhost:3306/foodie-shop-dev"
userId="root"
password="root">
jdbcConnection>
<javaModelGenerator targetPackage="com.wjw.pojo" targetProject="src/main/java"/>
<sqlMapGenerator targetPackage="mapper" targetProject="src/main/resources"/>
<javaClientGenerator targetPackage="com.wjw.mapper" targetProject="src/main/java" type="XMLMAPPER"/>
<table tableName="carousel">table>
<table tableName="category">table>
<table tableName="items">table>
<table tableName="items_comments">table>
<table tableName="items_img">table>
<table tableName="items_param">table>
<table tableName="items_spec">table>
<table tableName="order_items">table>
<table tableName="order_status">table>
<table tableName="orders">table>
<table tableName="user_address">table>
<table tableName="users">table>
context>
generatorConfiguration>
把自动生成的mapper、pojo、resources/mapper文件夹拷贝到我们的主项目中。
第一个导入foodie-dev-mapper的com.wjw.mapper包下
第三个导入foodie-dev-mapper的resources/mapper下
第二个导入foodie-dev-pojo的com.wjw.pojo包下
<dependency>
<groupId>tk.mybatisgroupId>
<artifactId>mapper-spring-boot-starterartifactId>
<version>2.1.5version>
dependency>
############################################################
#
# mybatis mapper 配置
#
############################################################
# 通用 Mapper 配置
mapper:
mappers: com.wjw.my.mapper.MyMapper
not-empty: false # 在进行数据库操作时,判断表达式 username !=null,是否追加 username != ''
identity: MYSQL
package com.wjw.my.mapper;
import tk.mybatis.mapper.common.Mapper;
import tk.mybatis.mapper.common.MySqlMapper;
/**
* 继承自己的MyMapper
*/
public interface MyMapper<T> extends Mapper<T>, MySqlMapper<T> {
}
运行Application启动成功。
interface MyMapper<T> extends Mapper<T>, MySqlMapper<T>
这里有两个父类, Mapper 与MySqlMapper ,我们可以打开MySqlMapper 看一下:
public interface MySqlMapper<T> extends InsertListMapper<T>, InsertUseGeneratedKeysMapper<T> {
}
这里面又继承了了两个mapper,从类名上可以看得出来,是用于操作数据库的,这两个类里又分别包含了如下方法,简单归类一下:
很明显,在传统JavaWeb开发,这两个方法使用是没有问题的,但是我们的数据库表主键设计肯定是全局唯一的,所以不可能使用自增长id,所以这两个方法在我们开发过程中是不会使用的。
public interface Mapper<T> extends BaseMapper<T>, ExampleMapper<T>, RowBoundsMapper<T>, Marker {
}
Restful是一种通信方式,在系统与系统之间可以传递相应的消息,客户端与服务器端通信载体也是可以使用Restful WebService的。
Restful WebService的一个特点是无状态,服务器接收客户端请求时,服务器不需要了解这个request之前做过什么以及将来可能做什么。
使用Restful之后系统有独立性,可以做拆分。
Rest设计规范:
/order/{id}
-> /getOrder?id=1001
/order
-> /saveOrder
/order/{id}
-> /modifyOrder
/order/{id}
-> /deleteOrder?id=1001
使用@Transactional
注解来实现事务,查看源码:
默认值是Propagation.REQUIRED
,即如果当前没有事务则新建一个,修饰的方法必须运行在一个事务中,当前有事务的话会加入到现有的事务中,事务一共有7类:
分布式系统表的主键一般不设为自增,并且多设计成varchar
类型的,原因是要保证全局唯一性。
先从service层写起,编辑foodie-dec-service项目:
创建UserService接口:
package com.wjw.service;
/**
* 2 * @Author: 小王同学
* 3 * @Date: 2020/12/21 17:38
* 4
*/
public interface UserService {
/**
* 判断用户名是否存在
*/
public boolean queryUsernameIsExist(String username);
}
UserService接口的实现类:
package com.wjw.service.impl;
import com.wjw.mapper.UsersMapper;
import com.wjw.pojo.Users;
import com.wjw.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import tk.mybatis.mapper.entity.Example;
/**
* 2 * @Author: 小王同学
* 3 * @Date: 2020/12/21 17:39
* 4
*/
public class UserServiceImpl implements UserService {
@Autowired
public UsersMapper usersMapper;
/**
* 判断用户名是否存在
*
* @param username
*/
@Transactional(propagation = Propagation.SUPPORTS)
@Override
public boolean queryUsernameIsExist(String username) {
Example userExample = new Example(Users.class);
Example.Criteria userCriteria = userExample.createCriteria();
userCriteria.andEqualTo("username", username);
Users result = usersMapper.selectOneByExample(userExample);
return result != null;
}
}
在主项目的pom中添加依赖:
<dependency>
<groupId>commons-codecgroupId>
<artifactId>commons-codecartifactId>
<version>1.11version>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-lang3artifactId>
<version>3.4version>
dependency>
<dependency>
<groupId>commons-iogroupId>
<artifactId>commons-ioartifactId>
<version>1.3.2version>
dependency>
在foodie-dev-api项目中定义controller:
package com.wjw.controller;
import com.wjw.service.UserService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 2 * @Author: 小王同学
* 3 * @Date: 2020/12/20 16:29
* 4
*/
@RestController
@RequestMapping("passport")
public class PassportController {
@Autowired
private UserService userService;
@GetMapping("/usernameIsExist")
public int usernameIsExist(@RequestParam String username) {
// 判断用户名不能为空
if (StringUtils.isBlank(username)){
return 500;
}
// 查找注册的用户名是否存在
boolean isExist = userService.queryUsernameIsExist(username);
if (isExist) {
return 500;
}
// 用户名没有重复
return 200;
}
}
在foodie-dev-common模块中创建com.wjw.utils包,并定义响应数据结构。
package com.wjw.utils;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* @Description: 自定义响应数据结构
* 本类可提供给 H5/ios/安卓/公众号/小程序 使用
* 前端接受此类数据(json object)后,可自行根据业务去实现相关功能
*
* 200:表示成功
* 500:表示错误,错误信息在msg字段中
* 501:bean验证错误,不管多少个错误都以map形式返回
* 502:拦截器拦截到用户token出错
* 555:异常抛出信息
* 556: 用户qq校验异常
*/
public class WJWJSONResult {
// 定义jackson对象
private static final ObjectMapper MAPPER = new ObjectMapper();
// 响应业务状态
private Integer status;
// 响应消息
private String msg;
// 响应中的数据
private Object data;
@JsonIgnore
private String ok; // 不使用
public static WJWJSONResult build(Integer status, String msg, Object data) {
return new WJWJSONResult(status, msg, data);
}
public static WJWJSONResult build(Integer status, String msg, Object data, String ok) {
return new WJWJSONResult(status, msg, data, ok);
}
public static WJWJSONResult ok(Object data) {
return new WJWJSONResult(data);
}
public static WJWJSONResult ok() {
return new WJWJSONResult(null);
}
public static WJWJSONResult errorMsg(String msg) {
return new WJWJSONResult(500, msg, null);
}
public static WJWJSONResult errorMap(Object data) {
return new WJWJSONResult(501, "error", data);
}
public static WJWJSONResult errorTokenMsg(String msg) {
return new WJWJSONResult(502, msg, null);
}
public static WJWJSONResult errorException(String msg) {
return new WJWJSONResult(555, msg, null);
}
public static WJWJSONResult errorUserQQ(String msg) {
return new WJWJSONResult(556, msg, null);
}
public WJWJSONResult() {
}
public WJWJSONResult(Integer status, String msg, Object data) {
this.status = status;
this.msg = msg;
this.data = data;
}
public WJWJSONResult(Integer status, String msg, Object data, String ok) {
this.status = status;
this.msg = msg;
this.data = data;
this.ok = ok;
}
public WJWJSONResult(Object data) {
this.status = 200;
this.msg = "OK";
this.data = data;
}
public Boolean isOK() {
return this.status == 200;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public String getOk() {
return ok;
}
public void setOk(String ok) {
this.ok = ok;
}
}
之后重新修改刚刚的PassportController
package com.wjw.controller;
import com.wjw.service.UserService;
import com.wjw.utils.WJWJSONResult;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 2 * @Author: 小王同学
* 3 * @Date: 2020/12/20 16:29
* 4
*/
@RestController
@RequestMapping("passport")
public class PassportController {
@Autowired
private UserService userService;
@GetMapping("/usernameIsExist")
public WJWJSONResult usernameIsExist(@RequestParam String username) {
// 判断用户名不能为空
if (StringUtils.isBlank(username)){
return WJWJSONResult.errorMsg("用户名不能为空");
}
// 查找注册的用户名是否存在
boolean isExist = userService.queryUsernameIsExist(username);
if (isExist) {
return WJWJSONResult.errorMsg("用户名已经存在");
}
// 用户名没有重复
return WJWJSONResult.ok();
}
}
只要是前端传向后端接收的数据体,都可以统一的定义为XxxBO
在foodie-dev-pojo项目下创建bo类:
package com.wjw.pojo.bo;
/**
* 2 * @Author: 小王同学
* 3 * @Date: 2020/12/21 20:41
* 4
*/
public class UserBO {
private String username;
private String password;
private String confirmPassword;
......
}
对应注册时填写的数据
在common模块中引入一些工具:
引入MD5加密工具:
package com.wjw.utils;
import org.apache.commons.codec.binary.Base64;
import java.security.MessageDigest;
public class MD5Utils {
/**
*
* @Title: MD5Utils.java
* @Package com.wjw.utils
* @Description: 对字符串进行md5加密
*/
public static String getMD5Str(String strValue) throws Exception {
MessageDigest md5 = MessageDigest.getInstance("MD5");
String newstr = Base64.encodeBase64String(md5.digest(strValue.getBytes()));
return newstr;
}
public static void main(String[] args) {
try {
String md5 = getMD5Str("wjw");
System.out.println(md5);
} catch (Exception e) {
e.printStackTrace();
}
}
}
引入操作日期的工具:
点击获取
引入表示性别的枚举类:
package com.wjw.enums;
/**
* 2 * @Author: 小王同学
* 3 * @Date: 2020/12/21 21:04
* 4
*/
public enum Sex {
woman(0, "女"),
man(1, "女"),
secret(2, "女");
public final Integer type;
public final String value;
Sex(Integer type, String value) {
this.type = type;
this.value = value;
}
}
引入生成全局唯一ID的三个包(以后会详细讨论,现在先用):
点击获取
不要忘了在启动类上添加注解来扫描这个包。
package com.wjw;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
import tk.mybatis.spring.annotation.MapperScan;
/**
* 2 * @Author: 小王同学
* 3 * @Date: 2020/12/20 15:11
* 4
*/
@SpringBootApplication
@MapperScan(basePackages = "com.wjw.mapper")
@ComponentScan(basePackages = {
"com.wjw", "org.n3r.idworker"})
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@Autowired
private Sid sid;
/**
* 创建用户
*
* @param userBO
* @return
*/
@Transactional(propagation = Propagation.REQUIRED)
@Override
public Users createUser(UserBO userBO) {
String userId = sid.nextShort();
Users user = new Users();
user.setId(userId);
user.setUsername(userBO.getUsername());
try {
user.setPassword(MD5Utils.getMD5Str(userBO.getPassword()));
} catch (Exception e) {
e.printStackTrace();
}
// 默认用户昵称同用户名
user.setNickname(userBO.getUsername());
// 默认头像
user.setFace(USER_FACE);
// 默认生日
user.setBirthday(DateUtil.stringToDate("1900-01-01"));
// 默认性别 保密
user.setSex(Sex.secret.type);
user.setCreatedTime(new Date());
user.setUpdatedTime(new Date());
usersMapper.insert(user);
// 返回用于在页面里显示用户基本信息
return user;
}
在PassportController
中创建注册方法:
@PostMapping("/regist")
public WJWJSONResult regist(@RequestBody UserBO userBO) {
String username = userBO.getUsername();
String password = userBO.getPassword();
String confirmPwd = userBO.getConfirmPassword();
// 判断用户名和密码必须不为空
if (StringUtils.isBlank(username) || StringUtils.isBlank(password) || StringUtils.isBlank(confirmPwd)){
return WJWJSONResult.errorMsg("用户名或密码不能为空");
}
// 查询用户名是否存在
boolean isExist = userService.queryUsernameIsExist(username);
if (isExist) {
return WJWJSONResult.errorMsg("用户名已经存在");
}
// 密码长度不能少于6位
if (password.length() < 6) {
return WJWJSONResult.errorMsg("密码长度不能少于6");
}
// 判断两次密码是否一致
if (!password.equals(confirmPwd)) {
return WJWJSONResult.errorMsg("两次密码输入不一致");
}
// 实现注册
userService.createUser(userBO);
// TODO 生成用户token,存入redis会话
// TODO 同步购物车数据
return WJWJSONResult.ok();
}
为了减少程序员撰写文档时间,提高生产力, Swagger2 应运而生,使用Swagger2 可以减少编写过多的文档,只需要通过代码就能生成文档API,提供给前端人员常方便。
在父工程pom中添加依赖:
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger2artifactId>
<version>2.4.0version>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger-uiartifactId>
<version>2.4.0version>
dependency>
<dependency>
<groupId>com.github.xiaoymingroupId>
<artifactId>swagger-bootstrap-uiartifactId>
<version>1.6version>
dependency>
在foodie-dev-api项目中创建Swagger配置类:
package com.wjw.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
/**
* 2 * @Author: 小王同学
* 3 * @Date: 2020/12/21 22:22
* 4
*/
@Configuration
@EnableSwagger2
public class Swagger2 {
// 配置swagger2核心配置
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2) // 指定api类型为swagger2
.apiInfo(apiInfo()) // 用于定义api文档汇总信息
.select()
.apis(RequestHandlerSelectors.basePackage("com.wjw.controller")) // 指定controller包
.paths(PathSelectors.any()) // 所有controller
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("天天吃货电商平台接口api")
.contact(new Contact("wjw", "https://www.baidu.com", "[email protected]")) // 联系人信息
.description("专为天天吃货提供的api文档")
.version("1.0.1")
.termsOfServiceUrl("http://shop.wjwqxy.cn/")
.build();
}
}
原路径:http://localhost:8088/swagger-ui.html
swagger-bootstrap-ui提供的界面:http://localhost:8088/doc.html
@ApiIgnore
可以忽略掉某个controller不在文档中显示
@Api(value = "注册登录", tags = {
"用于注册登录的相关接口"})
@ApiOperation(value = "用户名是否存在", notes = "用户名是否存在", httpMethod = "GET")
@ApiOperation(value = "用户注册", notes = "用户注册", httpMethod = "POST")
@ApiModel(value = "用户对象BO", description = "从客户端,由用户传入的数据封装在此entity中")
在该类的属性上添加注释
@ApiModelProperty(value = "用户名", name = "username", example = "wjw", required = true)
private String username;
@ApiModelProperty(value = "密码", name = "password", example = "123456", required = true)
private String password;
@ApiModelProperty(value = "确认密码", name = "confirmPassword", example = "123456", required = true)
private String confirmPassword;
把前端源码拷贝到tomcat文件夹下的webapps里,启动bin文件夹下的startup.bat
以注册服务为例,先修改接口服务接口地址
在foodie-dev-api中配置跨域:
package com.wjw.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
/**
* 2 * @Author: 小王同学
* 3 * @Date: 2020/12/23 9:43
* 4
*/
@Configuration
public class CorsConfig {
public CorsConfig() {
}
@Bean
public CorsFilter corsFilter() {
// 1. 添加cors配置信息
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOrigin("http://localhost:8080");
// 设置是否发送cookie信息
config.setAllowCredentials(true);
// 设置允许请求的方式
config.addAllowedMethod("*");
// 设置允许的header
config.addAllowedHeader("*");
// 2. 为url添加映射路径
UrlBasedCorsConfigurationSource corsSource = new UrlBasedCorsConfigurationSource();
corsSource.registerCorsConfiguration("/**",config);
return new CorsFilter(corsSource);
}
}
从service层开始写起,在UserService中添加接口:
/**
* 检索用户名密码是否匹配,用于登录
* @param username
* @param password
* @return
*/
public Users queryUserForLogin(String username, String password);
在UserServiceImpl中实现接口:
/**
* 检索用户名密码是否匹配,用于登录
*
* @param username
* @param password
* @return
*/
@Override
public Users queryUserForLogin(String username, String password) {
Example userExample = new Example(Users.class);
Example.Criteria userCriteria = userExample.createCriteria();
userCriteria.andEqualTo("username", username);
userCriteria.andEqualTo("password", password);
Users result = usersMapper.selectOneByExample(userExample);
return result;
}
由于登录时没有确认密码,所以UserBo类的定义中confirmPassword字段的required属性设为false
实现相应的controller方法(PassportController中):
@ApiOperation(value = "用户登录", notes = "用户登录", httpMethod = "POST")
@PostMapping("/login")
public WJWJSONResult login(@RequestBody UserBO userBO) throws Exception {
String username = userBO.getUsername();
String password = userBO.getPassword();
// 判断用户名和密码必须不为空
if (StringUtils.isBlank(username) || StringUtils.isBlank(password)){
return WJWJSONResult.errorMsg("用户名或密码不能为空");
}
// 实现登录
Users userResult = userService.queryUserForLogin(username, MD5Utils.getMD5Str(password));
if (userResult == null) {
return WJWJSONResult.errorMsg("用户名或密码不正确");
}
return WJWJSONResult.ok(userResult);
}
@GetMapping("/setSession")
public Object setSession(HttpServletRequest request) {
HttpSession session = request.getSession();
session.setAttribute("userInfo", "new user");
session.setMaxInactiveInterval(3600);
session.getAttribute("userInfo");
// session.removeAttribute("userInfo");
return "ok";
}
一些私密的信息不需要返回给前端:
/**
* 保护隐私
* @param userResult
* @return
*/
private Users setNullProperty(Users userResult) {
userResult.setPassword(null);
userResult.setMobile(null);
userResult.setEmail(null);
userResult.setCreatedTime(null);
userResult.setUpdatedTime(null);
userResult.setBirthday(null);
return userResult;
}
导入操作cookie的工具类以及将对象转为Json字符串的工具类:
CookieUtils.java和JsonUtils
controller中添加登录方法:
@ApiOperation(value = "用户登录", notes = "用户登录", httpMethod = "POST")
@PostMapping("/login")
public WJWJSONResult login(@RequestBody UserBO userBO,
HttpServletRequest request,
HttpServletResponse response) throws Exception {
String username = userBO.getUsername();
String password = userBO.getPassword();
// 判断用户名和密码必须不为空
if (StringUtils.isBlank(username) || StringUtils.isBlank(password)){
return WJWJSONResult.errorMsg("用户名或密码不能为空");
}
// 实现登录
Users userResult = userService.queryUserForLogin(username, MD5Utils.getMD5Str(password));
// 保护隐私
userResult = setNullProperty(userResult);
// 设置cookie,最后参数是开启加密
CookieUtils.setCookie(request, response, "user",
JsonUtils.objectToJson(userResult), true);
if (userResult == null) {
return WJWJSONResult.errorMsg("用户名或密码不正确");
}
return WJWJSONResult.ok(userResult);
}
测试:
同样在注册controller中也添加cookie
运行测试会发现注册完毕后自动登录。
1. 移除默认日志
要整合必须先把starter-logging剔除
2. 添加日志框架依赖
<dependency>
<groupId>org.slf4jgroupId>
<artifactId>slf4j-apiartifactId>
<version>1.7.21version>
dependency>
<dependency>
<groupId>org.slf4jgroupId>
<artifactId>slf4j-log4j12artifactId>
<version>1.7.21version>
dependency>
3. 创建 log4j.properties 并且放到api模块资源文件目录src/main/resources
log4j.rootLogger=DEBUG,stdout,file
log4j.additivity.org.apache=true
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.threshold=INFO
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%-5p %c{1}:%L - %m%n
log4j.appender.file=org.apache.log4j.DailyRollingFileAppender
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.DatePattern='.'yyyy-MM-dd-HH-mm
log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n
log4j.appender.file.Threshold=INFO
log4j.appender.file.append=true
log4j.appender.file.File=/workspaces/logs/foodie-api/mylog.log
利用apo技术来实现。
父项目的pom中引入依赖:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
dependency>
在api项目中创建辅助切面类:
AOP通知:
使用@Around
注解,切面表达式:
execution 代表所要执行的表达式主体
package com.wjw.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
/**
* 2 * @Author: 小王同学
* 3 * @Date: 2020/12/23 19:02
* 4
*/
@Aspect
@Component
public class ServiceLogAspect {
public static final Logger log = LoggerFactory.getLogger(ServiceLogAspect.class);
/**
* 切面表达式:
* execution 代表所要执行的表达式主体
* 第一处 * 代表方法返回类型 *代表所有类型
* 第二处 包名 代表aop监控的类所在的包
* 第三处 .. 代表该包及其子包下的所有类方法
* 第四处 * 代表类名, *代表所有类
* 第五处 *(..) *代表类中的方法名,(..)表示方法中的任何参数
* @param joinPoint
* @return
* @throws Throwable
*/
@Around("execution(* com.wjw.service.impl..*.*(..))")
public Object recordTimeLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("—————— 开始执行 {}.{} ——————", joinPoint.getTarget().getClass(), joinPoint.getSignature().getName());
// 记录开始时间
long begin = System.currentTimeMillis();
// 执行目标service
Object result = joinPoint.proceed();
// 记录结束时间
long end = System.currentTimeMillis();
long takeTime = end - begin;
if (takeTime > 3000) {
log.error("—————— 执行结束,耗时:{} 毫秒 ——————", takeTime);
}else if (takeTime > 2000){
log.warn("—————— 执行结束,耗时:{} 毫秒 ——————", takeTime);
}else {
log.info("—————— 执行结束,耗时:{} 毫秒 ——————", takeTime);
}
return result;
}
}
PassportController中添加相应的方法:
@ApiOperation(value = "用户退出登录", notes = "用户退出登录", httpMethod = "POST")
@PostMapping("/logout")
public WJWJSONResult logout(@RequestParam String userId,
HttpServletRequest request,
HttpServletResponse response) {
// 清除用户的相关信息的cookie
CookieUtils.deleteCookie(request, response, "user");
// TODO 用户退出登录,需清除购物车
// TODO 分布式会话中需要清除用户数据
return WJWJSONResult.ok();
}
修改api项目的配置文件: