实战项目-苍穹外卖
此项目由黑马程序员提供,此笔记用以记录在此实战项目中的知识点欠缺部分
Nginx
Nginx的功能其实有很多很多,我们在此项目中就简单应用于部署前端工程项目
我们在以前的JavaWeb学习中,前端工程项目是使用npm run dev功能直接测试运行
但是在实际生产环境中,前端项目和后端项目一样,需要扔进一个类似于Tomcat的容器中运行,这个容器就是Nginx
使用npm run build 命令即可将项目打包,将打包好的项目扔进nginx的html目录(静态资源目录)下,直接启动即可
反向代理
前端的请求路径为:http://localhost/api/employee/login
而后端实际的业务路径为:http://localhost:8080/admin/employee/login
请求路径并不一致,但却能成功请求,这里就用到了Nginx的重要功能之一——反向代理
负载均衡
1 2 3 4 5 6 7 8 9 10
| upstream webservers{ server 127.0.0.1:8080 weight=90 }
location /user/ { proxy_pass http://webservers/user/ }
|
负载均衡的策略
| 名称 |
说明 |
| 轮询 |
默认方式 |
| weight |
权重方式,默认为1,权重越高,被分配的客户端请求越多 |
| ip_hash |
依据ip分配方式,这样每个访客可以固定访问一个后端服务 |
| least_conn |
依据最少连接方式,把请求优先分配给连接数少的后端服务 |
| url_hash |
依据url分配方式,这样相同的url会被分配到同一个后端服务 |
| fair |
依据响应时间方式,响应时间短的服务将会被优先分配 |
分模块设计
我们之前都是一个模块下用包结构来区分功能层级,但包多了就容易混淆
这个项目使用模块来区分功能层级,使用maven的聚合功能(modules)来实现子模块的统一构建
在父工程中,使用dependencyManagement进行版本管理
- common:用以存放工具类
- pojo:存放实体类
- server:用于存放三层架构,做web业务处理
这样使用模块分层后,每一个模块根据需求单独导入依赖,结构清晰,更容易理解
POJO
在之前的学习中,我们也学到Vo的作用,其实pojo分为三种实体类型
- Entity:实体,通常跟数据库中表对应
- DTO:数据传输对象,通常用于程序中各层之间的数据传输,比如接收前端传过来的JSON
- VO:视图对象,为前端展示数据提供的对象,通常用于封装数据库查询结果
我们在之前接收请求的参数,是直接使用实体类来接收
但如果前端提交的数据和实体类中对应的属性差别比较大时,建议采用DTO来封装数据
但在Service层交给mapper层处理数据的时候,应该将DTO转换为实体类对象
BeanUtils
SpringBoot提供了此工具类,可调用copyProperties()方法对对象进行属性拷贝
要求对象中属性名完全一致
1
| BeanUtils.copyProperties(employeeDTO,employee);
|
Lombok
@Builder,lombok提供的实体类中注解,让我们可以通过链式调用给对象中的属性进行赋值
1 2 3 4 5 6
| EmployeeLoginVO.builder() .id(employee.getId()) .userName(employee.getUsername()) .name(employee.getName()) .token(token) .build();
|
MD5加密
我们在之前一直使用自己提供的MD5Util对明文密码进行加密后存入数据库
SPring给我们提供了此工具类——DigestUtils,其中的md5DigestAsHex()可以将一个字符数组转换成为md5加密后的字符串
digest中文释义:消化
1
| password = DigestUtils.md5DigestAsHex(password.getBytes());
|
接口管理工具
YApi Pro-高效、易用、功能强大的可视化接口管理平台
Apifox - API 文档、调试、Mock、测试一体化协作平台。拥有接口文档管理、接口调试、Mock、自动化测试等功能,接口开发、测试、联调效率,提升 10 倍。最好用的接口文档管理工具,接口自动化测试工具。
Swagger&knife4j
早期,swagger-boostrap-ui是1.x版本,如今swagger-bootsrap-ui到2.x,同时也更改名字Knife4j,适用于单体和微服务项目
knife4j就相当于一个项目内置的postman,它可以生成接口文档,生成接口所需参数,直接在项目内对后端接口做测试
快速开始 | Knife4j (xiaominfo.com)
WebConfig配置类
在配置类中注入Bean,将配置好的knife4j注入到IoC容器中
通过配置静态资源映射的方式,可以直接通过url访问到接口文档
http://localhost:8080/doc.html
我们可以通过.groupName对用户端和管理端接口分组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
|
@Bean public Docket docket1() { ApiInfo apiInfo = new ApiInfoBuilder() .title("苍穹外卖项目接口文档") .version("2.0") .description("苍穹外卖项目接口文档") .build(); Docket docket = new Docket(DocumentationType.SWAGGER_2) .groupName("管理端接口") .apiInfo(apiInfo) .select() .apis(RequestHandlerSelectors.basePackage("com.sky.controller.admin")) .paths(PathSelectors.any()) .build(); return docket; }
@Bean public Docket docket2() { ApiInfo apiInfo = new ApiInfoBuilder() .title("苍穹外卖项目接口文档") .version("2.0") .description("苍穹外卖项目接口文档") .build(); Docket docket = new Docket(DocumentationType.SWAGGER_2) .groupName("用户端接口") .apiInfo(apiInfo) .select() .apis(RequestHandlerSelectors.basePackage("com.sky.controller.user")) .paths(PathSelectors.any()) .build(); return docket; }
protected void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/"); registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/"); }
|
常用注解
| 注解 |
说明 |
| @Api |
用在类上,例如Controller,表示对类的说明 |
| @ApiOperation |
用在方法上,例如Controller的方法,说明方法的用途和作用 |
| @ApiModel |
用在实体类上,例如entity、DTO、VO |
| @ApiModelProperty |
用在实体类的属性上,描述属性信息 |
通过这些注解,就可以在接口文档中丰富内容和参数,让接口文档有更好的可读性
token问题
再进行业务功能测试的时候,有一些业务需要在登陆的状态下进行,需要验证你的token
获取登录的token后,在全局参数设置中添加token请求头,这样每次请求都会带着token去测试业务接口了
LogBack
SpringBoot内置了LogBack,所以直接使用slf4j进行日志处理即可
使用@Slf4j注解标识要使用打印日志功能的类
我们一般都会在controller层,使用log.info来打印一下接收到的参数是否为正确参数
在log.info处添加断点,可更直观的观测变量的变化
1
| log.info("新增员工:{}", employeeDTO);
|
主键值获取
我们在之前的学习中,从token中获取主键值,是通过在请求头中获取token值,使用工具类反向解密token获取主键值
现在,我们在拦截其中,直接通过token拿到了id值,现在思考如何将id值传到service层即可
1 2 3
| Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token); Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString()); log.info("当前员工id:", empId);
|
ThreadLocal
Thread的局部变量,每个线程都有自己单独的线程空间,具有线程隔离的效果
然而,Tomcat针对一次业务请求,分配的是同一个线程
在这个项目中,我们将对ThreadLocal局部变量的存取封装成一个工具类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public class BaseContext {
public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
public static void setCurrentId(Long id) { threadLocal.set(id); }
public static Long getCurrentId() { return threadLocal.get(); }
public static void removeCurrentId() { threadLocal.remove(); }
}
|
将拦截器中解析出来的id存放到ThreadLocal中,在service中取出使用即可
ThreadLocal在此就可以看作在一次请求中传递数据的变量即可,具有请求隔离(线程隔离)的效果
日期格式问题
在实现员工的分页查询功能时,我们从数据库中查找到的创建时间和修改时间的值不太正确
这是因为LocalDateTime类型数据直接放到json中返回给前端,达不到想要的日期格式效果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| "createTime": [ 2022, 2, 15, 15, 51, 20 ], "updateTime": [ 2022, 2, 17, 9, 16, 20 ]
|
有两种方法解决此问题
在属性上加入@JsonFormat注解,对日期进行格式化
1 2 3 4 5
| @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime createTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime updateTime;
|
这种方式处理的不够彻底,只能针对于单个类中的单个属性做格式化处理
扩展消息转换器
在配置类中扩展SpringMVC的消息转换器,统一对日期类型进行格式化处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
@Override protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) { MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); converter.setObjectMapper(jacksonObjectMapper); converters.add(0,converter); }
|
其中,对象转换器由项目本身提供,其中定义了一些序列化和反序列化的规则,比如jacksonObjectMapper对象中提供了日期格式转换的规则
公共字段自动填充
表中数据有一些字段是公共字段,比如创建人,创建时间,修改人,修改时间等
这些字段每次由我们手动填写就很麻烦,也可以将其利用AOP的思路抽取出来
当我们执行更新操作的时候,需要设置修改人,修改时间
当我们执行插入操作的时候,需要设置创建时间,创建人,修改时间,修改人四条属性
实现思路
使用枚举类型标注正在进行的操作是什么 (UPDATE,INSERT)
通过注解@AutoFill表示需要进行公共字段填充的方法(Mapper层)
自定义切面类AutoFillAspect,统一拦截加入注解的方法,通过反射为公共字段赋值
自定义注解
1 2 3 4 5 6 7 8 9
|
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface AutoFillAspect { OperationType value(); }
|
自定义切面
在我们利用反射来获取具体方法时,不再手写字符串,而是通过常量类的方式来填写,这样做有两个好处:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
|
@Aspect @Component @Slf4j public class AutoFillAspect { @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFillAspect)") public void autoFillPointCut(){
} @Before("autoFillPointCut()") public void autoFill(JoinPoint joinPoint){ log.info("开始进行公共字段的自动填充……"); MethodSignature signature = (MethodSignature) joinPoint.getSignature(); com.sky.annotation.AutoFillAspect autoFillAspect = signature.getMethod().getAnnotation(com.sky.annotation.AutoFillAspect.class); OperationType operationType = autoFillAspect.value();
Object[] args = joinPoint.getArgs(); if (args == null || args.length == 0){ return; } Object entity = args[0];
LocalDateTime now = LocalDateTime.now(); Long currentId = BaseContext.getCurrentId();
if (operationType == OperationType.INSERT){ try { Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class); Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class); Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class); Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class); setCreateTime.invoke(entity,now); setCreateUser.invoke(entity,currentId); setUpdateTime.invoke(entity,now); setUpdateUser.invoke(entity,currentId); } catch (Exception e) { e.printStackTrace(); } }else { try { Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class); Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
setUpdateTime.invoke(entity,now); setUpdateUser.invoke(entity,currentId); } catch (Exception e) { e.printStackTrace(); } } } }
|
阿里云对象存储服务OSS
Java_对象存储(OSS)-阿里云帮助中心 (aliyun.com)
用户在前端上传文件后,由前端将文件内容放在请求体中发送到后端
- 配置文件:配置OSS所需的配置文件
- 工具类:OSS具体操作的工具类
- 配置类:使用配置文件创建OSS工具类的对象,注入到IoC容器
- Controller:注入对象,使用对象进行文件上传业务
配置文件
和JWT的处理方式一样,我们通过配置属性类的方式读取配置文件中的字段,对配置进行封装成类的操作
在yaml中的-分割和类中驼峰命名的字段在SpringBoot中可以得到自动转换
配置属性类
1 2 3 4 5 6 7 8 9
| @Component @ConfigurationProperties(prefix = "sky.alioss") @Data public class AliOssProperties { private String endpoint; private String accessKeyId; private String accessKeySecret; private String bucketName; }
|
配置文件
1 2 3 4 5
| alioss: endpoint: ${sky.alioss.endpoint} access-key-id: ${sky.alioss.access-key-id} access-key-secret: ${sky.alioss.access-key-secret} bucket-name: ${sky.alioss.bucket-name}
|
配置类
使用配置类的方式,将AliOssUtil工具类的对象注入到IoC容器中
@ConditionalOnMissingBean注解来保证IoC容器中只存在一个此工具类
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Configuration @Slf4j public class OssConfiguration { @Bean @ConditionalOnMissingBean public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties) { log.info("开始创建阿里云文件上传工具类对象:{}", aliOssProperties); return new AliOssUtil(aliOssProperties.getEndpoint(), aliOssProperties.getAccessKeyId(), aliOssProperties.getAccessKeySecret(), aliOssProperties.getBucketName()); } }
|
业务代码
在controller中,使用@RequestPart MultipartFile file来接收文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| @Autowired private AliOssUtil aliOssUtil;
@PostMapping @ApiOperation("文件上传") public Result<String> upload(@RequestPart MultipartFile file) { log.info("文件上传:{}", file); try { String originalFilename = file.getOriginalFilename(); String substring = originalFilename.substring(originalFilename.lastIndexOf(".")); String objectName = UUID.randomUUID().toString() + substring;
String filePath = aliOssUtil.upload(file.getBytes(), objectName); return Result.success(filePath); } catch (IOException e) { log.error("文件上传失败:{}", e.getMessage()); } return null; }
|
小总结
学到这里,我们发现每一个插件的配置都是使用配置文件来配置
但生产环境与开发环境不同,为了解决这个问题,我们使用application-dev.yaml的方式,在主配置文件中激活
1 2 3
| spring: profiles: active: dev
|
我们发现配置文件直接使用到类中十分麻烦,就创建配置属性类来封装配置文件中的字段
使用@ConfigurationProperties(prefix = “sky.***”)注解来读取配置文件中的字段,填充到类中属性
对于工具类和其他框架的使用,不再实例化工具类直接使用,而是通过配置类将其配置好后注入IoC容器中,再由IoC中的其他组件调用
我们发现,很多自定义的字符串内容都使用常量或者枚举来提前定义,这样减少了书写错误,增加代码规范
我们发现,在进行数据库操作时,如果数据库表有存在自己的mapper(逻辑外键),则可以在Service中对两个mapper进行单独的sql操作,最后返回到service封装(涉及到多表查询,要在service上面加上事务)
如果使用联合查询,就写resultMap来映射数据库的表中列和实体类的属性对应,还有相同属性名的别名问题
当更新操作使用动态sql语句操作时,我们所有的更新操作都可以使用update这一条mapper,它会根据不同的参数而生成不同的sql进行数据更新
杨老师说,一张表的VO尽量只有一个,返回数据的时候可以一起返回空key,前端只需要用什么拿什么就好,这个VO中的其他表属性使用对象的形式存储在VO的属性中,这样就需要写resultMap来进行数据库列明——类中属性名的多层次映射
当controller层的用户端和管理端有相同的业务接口时,我们可以在@RestController注解后设置值的方式区分开
1
| @RestController("userShopController")
|
Redis
在Java中使用Redis:
- Jedis
- Lettuce
- Spring Data Redis
Spring Data Redis就是对Jedis和Lettuce进行高度的封装,我们直接使用即可
Spring Data Redis
导入依赖
1 2 3 4
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
|
配置Redis
在application.yaml中配置Redis
1 2 3 4 5
| spring: redis: host: 192.168.2.2 password: Zhuwenxue2002 database: 0
|
编写配置类,创建RedisTemplate对象*
如果不编写配置类的话,Redis也能按照默认配置运行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| package com.sky.config;
import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration @Slf4j public class RedisConfiguration { @Bean public RedisTemplate redisTemplate(RedisConnectionFactory factory) { log.info("开始创建redis模板对象"); RedisTemplate redisTemplate = new RedisTemplate(); redisTemplate.setConnectionFactory(factory); redisTemplate.setKeySerializer(new StringRedisSerializer()); return redisTemplate; } }
|
通过RedisTemplate对象操作Redis
需要注意的是,使用spring框架去进行redis操作会对数据进行序列化操作之后再存储,直接查看redis中的数据会有乱码情况出现
测试了String和hash类型的数据,其他类型的数据在使用上也大同小异
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| @SpringBootTest public class test { @Autowired private RedisTemplate redisTemplate;
@Test public void testRedisTemplate() { ValueOperations valueOperations = redisTemplate.opsForValue(); HashOperations hashOperations = redisTemplate.opsForHash(); ListOperations listOperations = redisTemplate.opsForList(); SetOperations setOperations = redisTemplate.opsForSet(); ZSetOperations zSetOperations = redisTemplate.opsForZSet(); }
@Test public void testString() { redisTemplate.opsForValue().set("key", "value"); String key = (String) redisTemplate.opsForValue().get("key"); System.out.println(key);
redisTemplate.opsForValue().set("key2", "value2", 3, TimeUnit.MINUTES);
redisTemplate.opsForValue().setIfAbsent("key3", "value3"); }
@Test public void testHash() { redisTemplate.opsForHash().put("1", "name", "xiaobai"); redisTemplate.opsForHash().put("1", "age", "20");
String name = (String) redisTemplate.opsForHash().get("1", "name");
System.out.println(name);
Set keys = redisTemplate.opsForHash().keys("1"); System.out.println(keys);
List values = redisTemplate.opsForHash().values("1"); System.out.println(values);
redisTemplate.opsForHash().delete("1", "age");
} }
|
HttpClient
我们在后端使用HttpClient来发送请求实现微信登陆功能
正常在使用HttpClient时,我们应该导入依赖,但阿里云OSS的依赖传递了此依赖,所以在此项目中无需额外导入

execute中文释义:执行
发送GET请求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| @Test public void testGet() throws IOException { CloseableHttpClient httpClient = HttpClients.createDefault();
HttpGet httpGet = new HttpGet("http://localhost:8080/user/shop/status");
CloseableHttpResponse response = httpClient.execute(httpGet);
int statusCode = response.getStatusLine().getStatusCode(); System.out.println(statusCode);
HttpEntity entity = response.getEntity(); String string = EntityUtils.toString(entity); System.out.println("string = " + string);
response.close(); httpClient.close();
}
|
发送Post请求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| @Test public void testPost() throws IOException { CloseableHttpClient httpClient = HttpClients.createDefault(); HttpPost httpPost = new HttpPost("http://localhost:8080/admin/employee/login");
JSONObject jsonObject = new JSONObject(); jsonObject.put("username", "admin"); jsonObject.put("password", "123456"); StringEntity entity = new StringEntity(jsonObject.toString()); entity.setContentType("application/json"); entity.setContentEncoding("UTF-8");
httpPost.setEntity(entity); CloseableHttpResponse response = httpClient.execute(httpPost); System.out.println(response.getStatusLine().getStatusCode());
HttpEntity entity1 = response.getEntity(); String string = EntityUtils.toString(entity1); System.out.println(string); response.close(); httpClient.close(); }
|
工具类
和其他依赖框架的使用思路一样,我们将HttpClient封装为工具类,想要使用的时候直接使用工具类即可