参照 我是一盘牛肉,和苍穹外卖的课程写的一些笔记。 推荐我是一盘牛肉的苍穹外卖的总结【苍穹外卖 | 项目日记】第九天 万字总结-CSDN博客
文件的分配:
项目分为 sky-common 、sky-pojo、sky-server 三个文件夹
sky-common 的作用:创建通用类
包名 | 作用 |
exception | 异常处理类的创建 |
constant | 定义常量类 |
context | 创建了一个用于在同一个线程中共享传递用户信息 |
enumeration | 设置需要用到的枚举类 |
json | 对象映射器 |
properties | 设置配置文件的参数 |
result | 返回类封装返回给前端的数据 |
utils | Utility(工具)的缩写 存放用到的工具类 |
Sky-pojo 将参数封装成Bean类
包名 | 作用 |
dto | 接口接收参数的类 |
entity | 和数据库表对应的java类 |
VO | 用于封装返回给前端的数据类 |
sky-server 业务处理
包名 | 作用 |
annotation | 存放自定义注释 |
aspect | 存放切面 |
config | 存放各种配置类 |
controller | 存放处理请求的方法 |
handler | 封装和处理异步任务,异常处理类 |
interceptor | 存放拦截器,按照指定要求拦截前端的请求 |
mapper | java与Mysql直接交互的包 |
service | 处理请求的具体实习逻辑 |
task | 定时任务类,指定时间自动执行 |
websocket | 封装websocket 简化websocket的使用 |
数据库表的作用
表名 | 作用 |
address_book | 存放用户设置的收货地址 |
category | 存放菜品的类型 |
dish | 存放菜品的信息 |
dish_flavor | 存放菜品的口味 |
employee | 存放管理端用户信息 |
order_detail | 存放订单的明细,有什么菜品 |
orders | 存放订单的信息 |
setmeal | 存放套餐信息 |
setmeal_dish | 存放套餐和菜品之间的关系 |
shopping_cart | 存放用户的购物车信息5 |
user | 存放用户端的用户信息 |
本项目在处理数据表之间的关系的时候,使用的是逻辑外键,不是物理外键。就是在创建数据表的时候不指定外键,而是在代码处理阶段,用代码的逻辑使表之间产生关系。
用到的技术:
JWT令牌加密技术:使用JWT令牌来识别用户身份
JWT(JSON WEB TOKEN) 是一种用来识别访问者身份信息的开放标准。由三部分组成 ,
标头(Header)记录令牌类型、签名算法
有效载荷(Payload)携带一些自定义信息、默认信息
签名(Signature)防止Token被篡改,确保安全性
token格式:head.payload.singurater 如:xxxxx.yyyy.zzzz
Base64:是一种基于64个可打印字符(字母大小写,0-9,+,/)来表示二进制数据的编码方式
JWT的认证过程:客户端向服务端发起请求 --> 服务端通过验证 --> 生成JWT令牌 --> 返回给前端 --> 前端访问时将令牌存放在请求头中 --> 拦截器拦截请求,检查令牌。
如果没有JWT : 用户不需要登录就直接使用请求路径,发起请求,后端的接口接收到请求就会执行,这样是不允许的。如:我使用删除用户的请求路径,后端接收到后就会执行,但是不知道是谁执行的,产生的后果,没法追责。使用要用到JWT进行身份验证。
导入JWT的相关依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
JWT的加密和解密 自定义工具类中设置的
public class JwtUtil {
/**
* 生成jwt
* 使用Hs256算法, 私匙使用固定秘钥
*
* @param secretKey jwt秘钥
* @param ttlMillis jwt过期时间(毫秒)
* @param claims 设置的信息
* @return
*/
public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) {
// 指定签名的时候使用的签名算法,也就是header那部分
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
// 生成JWT的时间
long expMillis = System.currentTimeMillis() + ttlMillis;
Date exp = new Date(expMillis);
// 设置jwt的body
JwtBuilder builder = Jwts.builder()
// 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
.setClaims(claims)
// 设置签名使用的签名算法和签名使用的秘钥
.signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))
// 设置过期时间
.setExpiration(exp);
return builder.compact();
}
/**
* Token解密
*
* @param secretKey jwt秘钥 此秘钥一定要保留好在服务端, 不能暴露出去, 否则sign就可以被伪造, 如果对接多个客户端建议改造成多个
* @param token 加密后的token
* @return
*/
public static Claims parserJWT(String secretKey, String token) {
// 得到DefaultJwtParser
Claims claims = Jwts.parser()
// 设置签名的秘钥
.setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))
// 设置需要解析的jwt
.parseClaimsJws(token).getBody();
return claims;
}
}
在拦截器中设置JWT令牌的校验
@Component
@Slf4j
public class JwtTokenAdminInterceptor implements HandlerInterceptor {
@Autowired
private JwtProperties jwtProperties;
/**
* 校验jwt
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//判断当前拦截到的是Controller的方法还是其他资源
if (!(handler instanceof HandlerMethod)) {
//当前拦截到的不是动态方法,直接放行
return true;
}
//1、从请求头中获取令牌
String token = request.getHeader(jwtProperties.getAdminTokenName());
//2、校验令牌
try {
log.info("jwt校验:{}", token);
Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
BaseContext.setCurrentId(empId);
log.info("当前员工id:{}", empId);
//3、通过,放行
return true;
} catch (Exception ex) {
//4、不通过,响应401状态码
response.setStatus(401);
return false;
}
}
}
在登录接口中使用JWT
@ApiOperation(value = "员工登录接口")
public Result<EmployeeLoginVO> login(@RequestBody EmployeeLoginDTO employeeLoginDTO) {
log.info("员工登录:{}", employeeLoginDTO);
// 根据用户名查询员工数据
Employee employee = employeeService.login(employeeLoginDTO);
//登录成功后,生成jwt令牌
Map<String, Object> claims = new HashMap<>();
claims.put(JwtClaimsConstant.EMP_ID, employee.getId());
String token = JwtUtil.createJWT(
jwtProperties.getAdminSecretKey(),
jwtProperties.getAdminTtl(),
claims);
//返回员工登录结果
EmployeeLoginVO employeeLoginVO = EmployeeLoginVO.builder()
.id(employee.getId())
.userName(employee.getUsername())
.name(employee.getName())
.token(token)
.build();
return Result.success(employeeLoginVO);
}
其他配置:如设置JWT的一些参数。如ttl(时间)、secreKey(密钥)、name(令牌名称)。这些数据如果使用硬编码(直接设置),就会导致后期维护难度增大。所以 设置一个类 交给spring book分配
//设置的属性值在配置文件中进行设置
package com.sky.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "sky.jwt") // 指定读取配置文件的前缀
@Data
public class JwtProperties {
/**
* 管理端员工生成jwt令牌相关配置
*/
private String adminSecretKey;
private long adminTtl;
private String adminTokenName;
/**
* 用户端微信用户生成jwt令牌相关配置
*/
private String userSecretKey;
private long userTtl;
private String userTokenName;
}
MD5数据加密
MD5(Message-Digest Algorithm 5)是一种广泛使用的哈希函数,可以生成128位的哈希值,通常表示为32个16进制字符。
MD5的特点:
1.固定长度:无论数据的大小,MD5始终生成128位的哈希值。
2.不可逆性:从哈希值无法直接还原原始数据。
3.雪崩效应:输入数据的微小变化会导致哈希值的巨大变化。
4.碰撞风险:不同的输入可能产生相同的哈希值。
使用:
//将前端传入的密码进行加密。存入数据库中的密码就是加密后的
password = DigestUtils.md5DigestAsHex(password.getBytes());
Nginx
Nginx 是一个高性能的Web服务器、反向代理服务器和负载均衡器,广泛用于现代Web架构。高并发、低内存占用和模块化设计。适用于静态资源服务、API网关、缓存加速等场景。
Nginx的核心功能
功能 | 说明 |
HTTP服务器 | 托管静态网站(HTML、CSS、JS)或者动态应用(PHP、python、Node.js) |
反向代理 | 将客户端请求转发到后端服务器(如Tomcat、Gunicorn、FastAPI) |
负载均衡 | 分配流量到多个后端服务器(轮询、权重、IP Hash等策略) |
SSL/TLS终止 | 处理THTTPS加密,减轻后端服务器压力 |
缓存加速 | 缓存静态资源或API响应,提高访问速度 |
URL重写 | 使用rewrite规则修改请求路径 |
访问控制 | 限制IP、用户认证、速率限制 |
Gzip压缩 | 减小传输数据量,提高加载速度 |
负载均衡:将客户端的请求分发到多个后端服务器,避免单台服务器过载,提高系统的可用性和吞吐量。
下面是一些常见的负载均衡的配置(设置分发逻辑)
算法 | 说明 |
轮询(Round Robin) | 依次分配请求(默认) |
加权轮询(Weighted Round Robin) | 根据服务器的性能不同分配权重 |
IP Hash | 相同的IP请求固定分配到某个服务器 |
最少连接 | 优先分配给连接数最少的服务器 |
示例: 在Nginx的conf配置文件中可见
upstream backend {
server 192.168.1.1:8080 weight=3; # 权重3(处理更多请求)
server 192.168.1.2:8080; # 默认权重1
server 192.168.1.3:8080 backup; # 备用服务器
}
反向代理:反向代理位于客户端和服务器之间的中间层,代表服务器处理客户端请求。客户端不直接访问服务器,而是访问反向代理,由代理转发请求并返回响应。
正向代理: 正向代理是客户端发送请求后交给代理客户端,由代理客户端向服务器发送请求并将响应返回给客户端。正向代理隐藏了客户端的真实信息和位置信息。
反向是区别于正向,代理就是类似于经纪人帮明星处理一些商务请求。
示例:
location /(地址) {
proxy_pass http://localhost:3000; # 转发到后端服务
proxy_set_header Host $host; # 传递原始域名
proxy_set_header X-Real-IP $remote_addr; # 传递客户端IP
}
基于Swagger 的Knife4j 注解
Knife4j是Swagger的增强方案。主要功能是提供注解生成API接口文档,并且能够进行接口测试。
常用注解:
注解 | 说明 |
@Api | 对Contorller类进行说明和描述,指定名称,描述,标签等等 |
@ApiOperation | 对Controller中的方法进行说明和描述,指定,名称,描述,请求类型 |
@ApiImplicitParam | 对方法中的参数进行说明和描述,指定,参数名称,类型,描述,是否必须 |
@ApiModel | 对请求或响应的数据的模型进行说明和描述,指定模型的名称,描述,数据类型 |
@ApiModelProperty | 对模型的属性进行说明和描述,指定模型的名称,描述,属性等 |
@ApiParam | 对方法的参数进行说明和描述 |
@ApiSupport | 对接口的作者和分组排序进行描述 |
在苍穹外卖项目中,在config包下的WebMvcConfiguration中配置Knife4j
/**
* 通过knife4j生成接口文档
* @return
*/
@Bean
public Docket docketAdmin() {
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;
}
再设置静态资源映射:
/**
* 设置静态资源映射
* @param registry
*/
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}
配置完成后就可以访问http://localhost:8080/doc.html,就可以看到生成的接口文档,并且可以对接口进行参数。
ThreadLocal
ThreadLocal是Java提供的一个工具类,用于在多线程环境下实现线程隔离和变量存储。每个线程可以独立的访问自己的变量副本,避免线程安全问题。
ThreadLocal的作用主要是在多线程环境下提供线程安全的变量访问。它通常解决线程间数据共享的问题,特别是在多个线程需要使用同一个变量时,可以确保每个线程访问的都是自己的变量副本,从而解决线程安全问题。
ThreadLocal的底层:由Thread管理ThreadLocalMap(不是继承Map,只是保存数据的结构类似),在ThreadLocalMap中保存 entry对象。entry对象的值为 key:线程、value:变量副本的值。
解决的问题大意如:情景:购票网站,有个共享变量A(余票),还有共享变量B,C(购票人,电话)
对于共享变量A:我们需要对其更改,就需要锁来防止多个线程同时使用,如:如果有两个线程同时访问最后一张票,那么两个线程得到的余票都是1,所以都能成功。但这是错误的。因此我们可以用锁来限制访问余票的线程,只能同时有一个线程使用。
对于共享变量B,C:每个购票人的姓名,电话都是不一样的,如果还使用锁,就会导致,线程1将数据1存入后,线程2读取了数据1,导致了线程安全问题。那么就要使用ThreadLocal 来解决这样的问题,线程1存入数据1后,就会生成变量副本1,线程2存入数据2和也会生成变量副本2。当线程1读取共享变量B,C时,就会读取变量副本1。线程2也是同理,会读取对应的变量副本2.
基本用法:
项目中,将ThreadLoacal的创建和方法都封装在BaseContext类中。这个类的创建也表达了,context这个包的作用是存放可以操作和访问程序上下文的各种包和接口。
package com.sky.context;
public class BaseContext {
// 创建线程局部变量 一个用户一个线程 这里指定了需要保存的变量是Long 类型的 id
public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
//设置当前用户id
public static void setCurrentId(Long id) {
threadLocal.set(id);
}
//获取当前用户id
public static Long getCurrentId() {
return threadLocal.get();
}
//删除当前用户id 防止线程复用
public static void removeCurrentId() {
threadLocal.remove();
}
}
在拦截器拦截前端的请求后,将令牌解析,得到员工的id然后将id存到ThreadLocal中。
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//判断当前拦截到的是Controller的方法还是其他资源
if (!(handler instanceof HandlerMethod)) {
//当前拦截到的不是动态方法,直接放行
return true;
}
//1、从请求头中获取令牌
String token = request.getHeader(jwtProperties.getAdminTokenName());
//2、校验令牌
try {
log.info("jwt校验:{}", token);
Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
//将当前登录用户的id保存到ThreadLocal中
BaseContext.setCurrentId(empId);
log.info("当前员工id:{}", empId);
//3、通过,放行
return true;
} catch (Exception ex) {
//4、不通过,响应401状态码
response.setStatus(401);
return false;
}
}
在需要使用的时候调用getCurrentId就可以得到对于的id。
基于消息转换器对时间进行格式化:
消息转换器:是消息中间件(如RabbitMQ、Kafka)和分布式系统中的关键组件,负责在消息生产者和消息消费者之间进行数据格式的转换,确保不同系统或服务能正确解析和处理消息。
解决的问题:
前端传递的时间参数的格式我们无法控制。因此就需要对时间参数进行格式化。
方法:
1.当需要格式化的情况较少。可以利用 @JsonFormet(pattern="yyyy-MM-dd HH:mm:ss")对某个属性进行格式化。
@JsonFormet(pattern = "yyyy-MM-dd")
private LocalDate localDate;
2.当需要修改的参数较多上面的方法就变得不便。因此选择在Spring MVC 中再扩展一个消息转换器,统一对发送给后端的时间进行处理。
/*
消息转换器
*/
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("开始进行消息转换器配置...");
//创建消息转换器对象
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
//设置对象转换器,底层使用Jackson将Java对象转为json
messageConverter.setObjectMapper(new JacksonObjectMapper());
//将上面的消息转换器对象追加到mvc框架的转换器集合中
converters.add(0, messageConverter);
}
基于PageHelper的分页查询:
PageHelper是基于java的开源框架,用于在MyBatis等持久层框架中方便的完成分页查询。
使用:
先导入Maven坐标
<!--MyBatis 分页插件,简化分页查询操作。-->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
</dependency>
在Controller层,(最好自己设置一个分页查询返回类)
@GetMapping ("/page")
@ApiOperation( "菜品分页查询")
public Result<PageResult> page(DishPageQueryDTO dishPageQueryDTO){
log.info("分页查询:{}", dishPageQueryDTO);
PageResult page = dishService.pageshow(dishPageQueryDTO);
return Result.success(page);
}
在service层,使用PageHelper.startPage接收 数据量 和 页码。然后使用Page(本质上是List)接收Mapper层的返回值。在这里也可以指定排序
/**
* 菜品分页查询
*
* @param dishPageQueryDTO
*/
@Override
public PageResult pageshow(DishPageQueryDTO dishPageQueryDTO) {
//分页查询
PageHelper.startPage( dishPageQueryDTO.getPage(),dishPageQueryDTO.getPageSize());
Page<DishVO> page= dishMapper.pageshow(dishPageQueryDTO);
return new PageResult(page.getTotal(),page.getResult());
}
Mapper层:
参数介绍:id=方法名 resultType=返回值类型
这里使用的是将两张表左连接,然后直接查询。注意在查询语句的最后不可以添加 ; 来表示结束,因为PageHelper会在查询语句的最后填充一些语句(拦截器+AOP)。
<select id="pageshow" resultType="com.sky.vo.DishVO">
select d.*, c.name as categoryName from sky_take_out.dish d
left join sky_take_out.category c on d.category_id = c.id
<where>
<if test="name != null and name != '' ">
AND d.name LIKE concat('%',#{name},'%')
</if>
<if test="categoryId!=null">
AND d.category_id= #{categoryId}
</if>
<if test="status!=null">
AND d.status= #{status}
</if>
</where>
</select>
基于注解和AOP的字段填充
在项目的修改和新建这两个操作中,总是有对于时间的设置,代码高度重复。因此就想要对这些代码进行简化。那么就使用了AOP,对需要操作的语句进行自动填充。当然在每个service类中都加入一个方法用于实现时间的操作,也行。但这还是太过麻烦,且又设置了许多不必要的方法。因此最终选择AOP进行解决。
解决方法:
因为是基于注解,那么我们先创建一个注解,这个注解的作用就是作为AOP的切入点表达式,用于AOP去寻找到需要填充的方法,并且在注解类中注册了一个返回值是枚举类的方法,用于区分不同类型的方法,实现不同的数据填充。
// 自动填充注解
@Target(ElementType.METHOD) // 注解在方法上
@Retention(RetentionPolicy.RUNTIME)// 注解在运行时
public
@interface AutoFill {
OperationType value(); // 操作类型
}
然后再实现AOP
@Aspect
@Component
public class AutoFillAspect {
/**
* 拦截需要自动填充的类
*/
//切入点表达式 1.execution(权限修饰符? 返回值 包名.类名.?方法名(方法参数)? throw 异常?)
// 2.annotation()按照注解的方式进行匹配
@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
public void autoFillPointCut() {} //使用了Pointcut ,使用方法名就可以引入切入点
/**
* 前置通知
* @param joinPoint
*/
@Before("autoFillPointCut()") // 前置通知
public void autoFill(JoinPoint joinPoint) { // JoinPoint 得到连接点信息
// 获取当前拦截的方法
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
AutoFill autoFill = methodSignature.getMethod().getAnnotation(AutoFill.class);
OperationType value = autoFill.value();
// 获取方法参数
Object[] args = joinPoint.getArgs();
if (args == null || args.length == 0) {
return;
}
Object object = args[0];// 获取第一个参数
// 根据对应的操作类型,为对应的属性赋值
LocalDateTime now = LocalDateTime.now();
Long currentId = BaseContext.getCurrentId();
if (value == OperationType.INSERT) {
// 插入操作,为插入时间,更新时间,创建人,更新人赋值
try {
Method setCreateTime = (Method) object.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME , LocalDateTime.class).invoke(object, now);
Method setUpdateTime = (Method) object.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class).invoke(object, now);
Method setCreateUser = (Method) object.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class).invoke(object, currentId);
Method setUpdateUser = (Method) object.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class).invoke(object, currentId);
} catch (Exception e) {
e.printStackTrace();
}
}else if (value == OperationType.UPDATE) {
// 更新操作,为更新时间,更新人赋值
try {
Method setUpdateTime = (Method) object.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class).invoke(object, now);
Method setUpdateUser = (Method) object.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class).invoke(object, currentId);
}catch ( Exception e)
{
e.printStackTrace();
}
}
}
}
使用阿里云OSS云存储服务
当我们想要使用一些照片或文件让别人也可以看见,一般是将照片保存在项目文件中(也就是本地),也可以将照片存放于云存储中,通过网络地址进行访问。这里就使用阿里云OSS来保存照片数据。 这里的代码较为固定,基本都可以复用。
步骤:
1.配置阿里云的一些配置。
在common包下,设置一些需要用到的参数。
package com.sky.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "sky.alioss")// 指定读取配置文件的前缀
@Data
public class AliOssProperties {
private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;
}
让这些参数在配置文件中得到设置。当Spring Boot启动后,自动将在配置文件中设置的参数,绑定到AliOssProperties类的参数中。
#application.yml
alioss:
access-key-id: ${sky.alioss.access-key-id}
endpoint: ${sky.alioss.endpoint}
access-key-secret: ${sky.alioss.access-key-secret}
bucket-name: ${ sky.alioss.bucket-name}
#application-dev.yml
alioss:
endpoint:
bucket-name:
access-key-id:
access-key-secret:
创建工具类对象; 固定
package com.sky.utils;
import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.io.ByteArrayInputStream;
@Data
@AllArgsConstructor
@Slf4j
public class AliOssUtil {
private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;
/**
* 文件上传
*
* @param bytes
* @param objectName
* @return
*/
public String upload(byte[] bytes, String objectName) {
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
try {
// 创建PutObject请求。
ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(bytes));
} catch (OSSException oe) {
System.out.println("Caught an OSSException, which means your request made it to OSS, "
+ "but was rejected with an error response for some reason.");
System.out.println("Error Message:" + oe.getErrorMessage());
System.out.println("Error Code:" + oe.getErrorCode());
System.out.println("Request ID:" + oe.getRequestId());
System.out.println("Host ID:" + oe.getHostId());
} catch (ClientException ce) {
System.out.println("Caught an ClientException, which means the client encountered "
+ "a serious internal problem while trying to communicate with OSS, "
+ "such as not being able to access the network.");
System.out.println("Error Message:" + ce.getMessage());
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
//文件访问路径规则 https://BucketName.Endpoint/ObjectName
StringBuilder stringBuilder = new StringBuilder("https://");
stringBuilder
.append(bucketName)
.append(".")
.append(endpoint)
.append("/")
.append(objectName);
log.info("文件上传到:{}", stringBuilder.toString());
return stringBuilder.toString();
}
}
创建配置类:
package com.sky.config;
import com.sky.properties.AliOssProperties;
import com.sky.utils.AliOssUtil;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration//声明配置类
@Slf4j// 日志
@ApiOperation( "阿里云文件上传配置类")
public class OssComfiguration {
@Bean//声明bean 交给spring容器管理
@ConditionalOnMissingBean// 判断当前容器中是否有这个bean
public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties) {
log.info("开始创建阿里云文件上传工具类对象:{}", aliOssProperties);
//创建阿里云文件上传工具类对象,将配置类中的参数传递给工具类对象
return new AliOssUtil(aliOssProperties.getEndpoint(),
aliOssProperties.getAccessKeyId(),
aliOssProperties.getAccessKeySecret(),
aliOssProperties.getBucketName());
}
}
阿里云OSS的使用
public Result<String> upload(MultipartFile file) throws IOException {
log.info("文件上传 {}" , file);
String originalFilename = file.getOriginalFilename();
String substring = originalFilename.substring(originalFilename.lastIndexOf("."));
String s = UUID.randomUUID().toString() + substring;
String upload = aliOssUtil.upload(file.getBytes(), s);
return Result.success(upload);
}
注意点:
1.使用UUID生成随机数防止图片的命名重复导致覆盖。
2.在配置文件中设置了开发时期的配置文件。设置不同的配置文件用于解决应用在不同时期的不同配置。我的application.yml文件配置的数据就是我要用到的数据,但数据的赋值并不是直接在application.yml文件中设置,而是在application-dev.yml中设置。这有利于在不同时期修改配置文件参数。
开启事务
开启事务用于解决修改数据库表的统一。当在一个方法中要修改多个表数据时,可能产生一个操作成功,另一个操作失败的情况,而这种情况就会导致数据表的数据不对应。因此要在这个方法中开启事务,来保证修改的统一。(要不同时修改成功,要不全修改失败)
使用方式:
1.在要开启事务的方法上添加注解 @Transactional
2.在启动类上添加注解 @EnableTransactionManagement
关于事务:
事务是数据库操作的核心概念,它确保一组1数据要么全部成功,要么全部回滚(失败)。在Spring框架中提供了两种事务管理方式:声明式和编程式。
Transactional的一些参数
属性 | 说明 | 示例 |
---|---|---|
propagation | 事务传播行为 | @Transactional(propagation = Propagation.REQUIRED) |
isolation | 事务隔离级别 | @Transactional(isolation = Isolation.READ_COMMITTED) |
timeout | 事务超时时间(秒) | @Transactional(timeout = 5) |
readOnly | 是否只读事务 | @Transactional(readOnly = true) |
rollbackFor | 指定哪些异常触发回滚 | @Transactional(rollbackFor = {SQLException.class}) |
noRollbackFor | 指定哪些异常不触发 | @Transactional(noRollbackFor = {NullPointerException.class}) |
事务传播行为
传播行为 | 说明 |
---|---|
REQUIRED(默认) | 如果当前没有事务,就新建一个事务;如果已存在事务,就加入该事务。 方法a设置注解,方法b没有设置。当方法a调用方法b时就会将方法b加入到方法a中 |
REQUIRES_NEW | 新建事务,如果当前有事务,则挂起当前事务。 |
SUPPORTS | 如果当前有事务,就加入事务;如果没有,就以非事务方式执行。 |
NOT_SUPPORTED | 以非事务方式执行,如果当前有事务,则挂起该事务。 |
MANDATORY | 必须在事务中运行,否则抛出异常。 |
NEVER | 不能在事务中运行,否则抛出异常。 |
NESTED | 如果当前有事务,则在嵌套事务中执行;否则新建事务。 |
事务隔离级别
隔离级别 | 说明 | 可能的问题 |
---|---|---|
DEFAULT | 使用数据库默认隔离级别(通常 READ_COMMITTED )。 | - |
READ_UNCOMMITTED | 允许读取未提交的数据(脏读)。 | 脏读、不可重复读、幻读 |
READ_COMMITTED | 只能读取已提交的数据(Oracle/PostgreSQL 默认)。 | 不可重复读、幻读 |
REPEATABLE_READ | 确保同一事务多次读取相同数据结果一致(MySQL 默认)。 | 幻读 |
SERIALIZABLE | 最高隔离级别,完全串行化执行。 | 性能低 |
使用redis将常用数据放入缓存,减少数据库查询,提高性能。
复习redis,查看大佬 我是一盘牛肉的博客redis的学习。
本项目使用redis的是:查询店铺营业状态和菜品展示和套餐等等数据。这两个地方都会出现多次查询的情况。这种通过少量操作可以调起大量后端操作的行为,是一个很危险的杠杆行为。在高并发的情况下,对服务器的压力就很大。
解决的思路就是通过将数据存储在缓存中,当访问功能时,先去缓存中查询数据,如果没有就再访问数据库并将数据保存到缓存。
代码实现:
查询店铺运营状态 使用redisTemplate.(不同的类型).(方法)
package com.sky.controller.user;
import com.sky.result.Result;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.*;
@Slf4j
@RestController("userShopController")
@RequestMapping("/user/shop")
public class ShopController {
@Autowired
private RedisTemplate redisTemplate;
@GetMapping("/status")
@ApiOperation("用户端获取店铺状态")
public Result getStatus(){
log.info("用户端获取店铺状态");
//规定key值 通过key和保存方式向缓存中查询数据
Integer status = (Integer)redisTemplate.opsForValue().get("SHOP_STATUS");
return Result.success(status);
}
}
菜品展示:
@RestController("userDishController")
@RequestMapping("/user/dish")
@Slf4j
@Api(tags = "C端-菜品浏览接口")
public class DishController {
@Autowired
private DishService dishService;
@Autowired
private RedisTemplate redisTemplate;
/**
* 根据分类id查询菜品
*
* @param categoryId
* @return
*/
@GetMapping("/list")
@ApiOperation("根据分类id查询菜品")
public Result<List<DishVO>> list(Long categoryId) {
log.info("根据分类id查询菜品:{}", categoryId);
String key = "dish_" + categoryId;
List<DishVO> dishVOList = (List<DishVO>) redisTemplate.opsForValue().get(key);
// 缓存存在 直接返回
if (dishVOList != null && dishVOList.size() > 0) {
return Result.success(dishVOList);
}
//缓存不存在
Dish dish = new Dish();
dish.setCategoryId(categoryId);
dish.setStatus(StatusConstant.ENABLE);//查询起售中的菜品
List<DishVO> list = dishService.listWithFlavor(dish);
//缓存数据
redisTemplate.opsForValue().set(key,list);
return Result.success(list);
}
}
如何确保你缓存的数据就是你数据库中的数据呢?就是如何保证缓存和数据表的一致?(借鉴我是一盘牛肉的笔记,大佬写的非常好。)
1.读写双写:在更新数据库数据的同时,也将缓存中的数据进行更新。
2.读写更新:在更新数据库数据时,先更新数据库,再异步更新缓存。这样提高了写入的性能,但会有一小段时间数据数据库数据和缓存不对应。
3.缓存失效:在设置缓存时,设置缓存过期时间。使得缓存会在一定时间内自动更新。但这种修改因为到了规定的过期时间才会更新,因此没法实时更新数据。
4.发布订阅模式:使用redis的发布订阅功能,当数据库中数据发生变化时,通过发布消息的方式通知订阅者(redis缓存)进行更新。
redis自身也存在问题
1.穿透(Cache Penetration):当一个不存在的键被频繁查询时,会导致缓存无效并且每次都会访问数据库。这样就会浪费数据库资源,还可能导致数据库压力过大。为了解决穿透问题,可以使用布隆过滤器或者在查询得到空结果后也进行缓存,设置一个较短的过期时间。
2.击穿(Cache Breakdown):当一个热点键过期或被清除时,同时又有大量的请求访问该键,导致这些请求都直接访问数据库,导致数据库压力增大。解决击穿问题,可以使用互斥锁或者分布式锁,保证只有一个请求那个访问数据库,并在请求获取数据后更新缓存。
3.雪崩(Cache Avalanche):当大量的缓存键在同一时间过期或者缓存服务器发生故障,导致大量请求直接访问数据库,称为雪崩。在这种情况下,数据库承受巨大的压力。为了解决雪崩问题,可以使用多级缓存架构,将请求分散到多个缓存服务器,或者使用热点数据预加载的方法,提前加载热门数据到缓存中。
利用Spring Cache来对业务进行优化
Spring Cache是Spring Framework 提供的缓存抽象层,它通过简单的注释就能为应用程序添加缓存功能,显著提升系统性能。
核心特点:
统一抽象层 | 支持多种缓存实现(Redis、EnCache、Caffeine等) |
声明式缓存 | 通过注解即可实现缓存功能 |
自动管理 | 自动处理缓存读写和失效 |
与Spring无缝集成 | 完美配合Spring事务等特性 |
有点像多口径炮(Spring Cache),不管装什么类型的炮弹(缓存技术),你只要做相同的操作就可以开炮。(完成缓存)
核心注解:
注解 | 说明 | 参数 |
@Cacheable | 启用缓存 | value:指定缓存名称 key:自定义缓存键的SpEL表达式 condition:执行缓存的条件 unless:否决缓存的条件 cacheManager:指定缓存管理器 |
@CacehEvict | 清除缓存 | allEntries:是否清空 beforeInvocation:是否在方法执行前清空 |
@CachePut | 更新缓存 | 和@Cacheable差不多 |
使用:
引入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
调用注解:
@PostMapping
@ApiOperation( "新增套餐")
// 和value一样 key的值可以是 参数,返回值
@CacheEvict(cacheNames = "Setmeal",key="#setmealDTO.id")
public Result save(@RequestBody SetmealDTO setmealDTO) {
log.info("新增套餐");
setmealService.save(setmealDTO);
return Result.success();
}
Httpclient:服务端进行Http通信的库,让后端像前端一样可以发送请求和接受响应
是一个常用技术。很多第三方接口的使用方式就是需要我们的后端发送请求到指定资源路径,这样才能调用相关服务。例如本项目的微信登录接口,后端使用登录凭证校验接口,后端在使用登录凭证校验接口的时候就需要发送指定请求到给定的URL中
使用:
添加依赖(Maven):
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>
Get请求:
public void testGet() throws Exception {
//创建HttpClient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
//创建请求对象
HttpGet httpGet = new HttpGet("http://127.0.0.1:8080/user/shop/status");
//设置请求头
httpGet.addHeader("User-Agent", "Mozilla/5.0");
//使用httpClient发送Get请求
CloseableHttpResponse execute = httpClient.execute(httpGet);
//得到响应
HttpEntity entity = execute.getEntity();
String body = EntityUtils.toString(entity);
System.out.println(body);
}
Post请求:
public void testPost() throws Exception{
//创建httpClient
CloseableHttpClient closeableHttpClient =HttpClients.createDefault();
//创建请求类
HttpPost httpPost =new HttpPost("http://localhost:8080/admin/employee/login");
//对post数据进行json封装
JSONObject jsonObject=new JSONObject();
jsonObject.put("username","admin");
jsonObject.put("password","123456");
StringEntity entity=new StringEntity(jsonObject.toString());
//指定请求编码方式
entity.setContentEncoding("utf-8");
//设置数据格式
entity.setContentType("application/json");
httpPost.setEntity(entity);
//发起请求
CloseableHttpResponse execute = closeableHttpClient.execute(httpPost);
HttpEntity entity1 = execute.getEntity();
String string = EntityUtils.toString(entity1);
System.out.println(string);
//关闭资源
execute.close();
closeableHttpClient.close();
}
微信登录接口
微信官方提供的小程序开发文档:关于微信登录接口的介绍: 小程序登录 | 微信开放文档
(图片来源于微信开发者文档)
流程:
1.小程序调用wx.login()获取code,code用于后续用户身份验证和获取用户信息。
2.小程序使用wx.request()将code发送到服务器
3.服务器接收code后将 appid + appsecret+code 封装成字符串,通过httpClient发送到微信 接口服务的指定URL。https://api.weixin.qq.com/sns/jscode2session
4.登录的API将session_key + openid等等数据响应给服务器
5.服务器将openid作为登录用户的唯一识别进行其他操作。
微信支付接口:
微信开发者文档中关于微信支付的介绍:微信支付 | 微信开放文档
非商户无法申请支付功能。在本项目实践中并未对其进行实现,而是通过修改前端的响应,来模拟实现支付功能。
Spring Task :轻量级定时任务调度模块
通过注解的方式设置指定时间,指定频率的实现某段程序。
启用定时任务:
在启动类上添加注释@EnableScheduling
创建定时任务类:
@Scheduled(cron ="0 * * * * ?" ) // 设置时间
public void Task(){
需要执行的代码 }
本项目中主要通过Spring Task 完成异常订单的处理:
@Component
@Slf4j
public class OrderTask {
@Autowired
private OrderMapper orderMapper;
/*
* 处理待付款超时情况
*/
@Scheduled(cron ="0 * * * * ?" ) // 设置时间
//需要执行的操作
public void ProcessOvertimeOrders(){
log.info("处理超时订单");
List<Orders> ordersList= orderMapper.getByStatusANDOrderTimeLT(Orders.PENDING_PAYMENT, LocalDateTime.now().plusMinutes(-15));
if (ordersList==null|| ordersList.isEmpty())return;
for (Orders orders : ordersList){
orders.setCancelTime(LocalDateTime.now());
orders.setStatus(Orders.CANCELLED);
orders.setCancelReason(MessageConstant.Order_timeout_automatic_cancellation);
orderMapper.update(orders);
}
}
@Scheduled(cron = "0 0 1 * * ?")
public void ProcessingCompletionIsNotConfirmed(){
log.info("处理完成未确认的订单");
List<Orders> ordersList=orderMapper.getByStatus(Orders.DELIVERY_IN_PROGRESS);
if (ordersList==null|| ordersList.isEmpty())return;
for (Orders orders :ordersList){
orders.setDeliveryTime(LocalDateTime.now());
orders.setStatus(Orders.COMPLETED);
orderMapper.update(orders);
}
}
}
处理异常订单逻辑:异常订单包括两种。
1.支付超时的订单:设置每5秒根据当前时间和订单提交时间进行比对,如果超时15分钟就将订单取消。
2.配送已完成但未更新的订单:规定在凌晨1点将所有以配送但未完成的订单,统一更新为已完成。(不太好,感觉应该配送2小时后就更新,但也有弊端)
主要对注解@Scheduled进行分析:
其中为cron参数赋值的字符串叫 cron表达式,其作用是设置时间
cron表达式的组成:秒 分 时 日 月 周 年。日和周不能同时设置,周是周几的意思,如果设置了日,那么可能出现日和周不对应的情况,所以大多情况是不用设置周的。
当然我们不需要自己去写cron表达式。因为有在线的cron生成器Cron - 在线Cron表达式生成器
使用WebSocket来实现用户端和服务端的通信:
webSocket是一种协议,用于在Web应用程序和服务器之间建立实时、双向的通信连接。它通过一个单一的TCP连接提供了持久化连接。
本项目使用webSocket的地方:用户端的催单和 管理端的来单提醒
创建WebSocket实现类
@serverEndpoint:指定要连接的请求接口;相当于声明这个类就是WebSocket实现类了
@Onopen:在连接成功时调用的方法
@OnMessage:在客户端发送信息后调用的方法
@OnClose:在连接断开后调用的方法
也可以自定义方法
@Component
@ServerEndpoint("/ws/{sid}") //连接的接口
public class WebSocketServer {
//存放会话对象
private static Map<String, Session> sessionMap = new HashMap();
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("sid") String sid) {
System.out.println("客户端:" + sid + "建立连接");
sessionMap.put(sid, session);
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, @PathParam("sid") String sid) {
System.out.println("收到来自客户端:" + sid + "的信息:" + message);
}
/**
* 连接关闭调用的方法
*
* @param sid
*/
@OnClose
public void onClose(@PathParam("sid") String sid) {
System.out.println("连接断开:" + sid);
sessionMap.remove(sid);
}
/**
* 群发
*
* @param message
*/
public void sendToAllClient(String message) {
Collection<Session> sessions = sessionMap.values();
for (Session session : sessions) {
try {
//服务器向客户端发送消息
session.getBasicRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
使用WebSocket实现类
因为在WebSocket实现类使用了@Component 将实现类交给了Spring容器管理,就可以使用注入来使用实现类。
@Override
public void reminder(Long id) throws Exception {
Orders orders = orderMapper.getById(id);
//通过WebSocket向管理端发信息
Map map = new HashMap<>();
map.put("type", 2); //1表示来一条消息 2表示催促订单
map.put("orderId", id);
map.put("content", "订单号:" + orders.getNumber());
String json = JSON.toJSONString(map);
webSocketServer.sendToAllClient(json);
}
除了webSocket来实现长连接,还有 Server-Sent Events (SSE)和 HTTP长轮询以及较为复杂的WebRTC。
SSE:是基于HTTP协议的一种单项实时通信技术,只能服务器向客户端通信。
HTTP长轮询:模拟实时通信的传统技术。是通过客户端发起请求,然后服务器就一直保持连接直到有数据或超时后断开,服务器返回响应后,客户端立即发起新的请求。但是这种方式只是模拟长连接,实现中会频繁向服务器发起请求,服务器资源消耗大,并且存在延迟。
WebRTC:实现的是点对点实时通信。点对点实时通信指的是浏览器和浏览器之间实现长连接,不经过服务器的处理,直接传输数据。
Apache POI技术实现导出文件:
Apache POI 提供 Java 库用于操作各种 Microsoft Office 格式文件,例如word文档(.dox / .docx ),Excel电子表格(.xls / .xlsx )以及PPT(.ppt / .pptx)。
使用POI感觉就像是软件测试的录制回放,代码的执行步骤大多是在模拟我们在使用可视化的应用时进行的操作。例如Excel 创建表格,创建第一页(sheet1),选择第几行第几列。
在项目中使用POI是为了生成运营报表,像这种固定格式的表格最好是直接使用模板,然后在模板的基础上添加数据,随后导入表格。
public void getExcel()throws Exception
{
//导入模板
InputStream in =this.getClass().getClassLoader().getResourceAsStream("template/运营数据报表模板.xlsx");
XSSFWorkbook excel = new XSSFWorkbook(in);//创建表格对象
XSSFSheet sheetAt = excel.getSheetAt(0);//创建页对象
//设置时间
LocalDateTime begin =LocalDateTime.of (LocalDate.now(),LocalTime.MIN).minusDays(30);
LocalDateTime end =LocalDateTime.of(LocalDate.now(),LocalTime.MAX).minusDays(1);
sheetAt.getRow(1).getCell(1).setCellValue(begin +"-----"+ end);
//填写表数据
//营业额 3-2
Double amount=reportMapper.getAmount(begin,end, Orders.COMPLETED);
amount = amount == null? 0.0 : amount;
sheetAt.getRow(3).getCell(2).setCellValue(amount);
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//下面的都是重复内容,和上面的操作一样,sheetAt.getRow(3).getCell(2).setCellValue(amount);
// 页对象 行 列 设置内容
LocalDate now =LocalDate.now();
//TODO 文件的下载地址,应该让用户选
String ip="E:\\Report\\"+now.toString()+"运营数据报表.xlsx";
FileOutputStream fos=new FileOutputStream(ip);
excel.write(fos);//将表格对象写入输出流,生成表格
excel.close();
fos.close();
}
在完成项目中我遇到的一些小问题:
1.使用注解书写sql语句:
@Update("update setmeal set status = #{status},update_time=#{updateTime},update_user=#{updateUser} where id = #{id}")
void setstartOrStop(Setmeal setmeal);
这个方法传入的参数是一个封装好的类,在sql语句中 用#{ } 来引用属性,
update_time是数据库中表格的属性 #{updateTime}是指使用传入的类的属性;规定java类属性的命名使用驼峰命名(单词之间连接使用大写)数据表中的属性命名单词之间使用下划线分隔
2.MyBaits使用XML配置文件书写sql语句
标准的头部声明,指定了版本,格式等信息
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
Mapper 中namespace参数指定当前这个xml配置文件,对应的是那个Mapper文件
<mapper namespace="com.sky.mapper.AddressBookMapper">
select:声明是查询语句; id:当前sql语句实现的对应的方法;
<select id="list" parameterType="AddressBook" resultType="AddressBook">
其他参数:
resultType:指定SQl返回结果的类型,自动映射到java对象或基本类型
<select id="getUserById" resultType="com.example.User"> SELECT * FROM user WHERE id = #{id} </select>
将查询的结果映射到User类上,字段名和属性名必须一致,否则无法映射。要求我们创建类时要与表对应。
resultMap: 自定义结果集映射规则,如解决resultType的字段名和属性名不一致情况
<resultMap id="userResultMap" type="User"> // 属性名 字段名 <id property="id" column="user_id"/>//处理主键字段 <result property="name" column="user_name"/>//处理普通字段 //映射关联对象 将关联对象结果映射到 dept 关联的对象类型是 Department <association property="dept" javaType="Department" resultMap="deptResultMap"/> //使用 deptResultMap来处理 dept的结果映射 </resultMap> <select id="getUserWithDept" resultMap="userResultMap"> SELECT u.id as user_id, u.name as user_name, d.* FROM user u LEFT JOIN department d ON u.dept_id = d.id </select>
useGeneratedKeys和keyProperty:将主键填到参数对象中
<insert id="insertUser" useGeneratedKeys="true" keyProperty="id"> INSERT INTO user (name) VALUES (#{name}) </insert>
Mapper方法中的参数是一个类,执行后回将主键值设置到传入的参数对象中
flushCache和useCache:控制二级缓存行为。
// 是否执行后清空缓存 是否缓存结果 <select id="getAllUsers" flushCache="false" useCache="true"> SELECT * FROM user </select>
二级缓存:简单的说是MyBatis提供的全家缓存机制。以namespace来区分二级缓存的范围,同一个namespace,缓存就是共用的。作用和我们自己设置的redis差不多,如果是第一次使用某SQL语句,就将结果存入二级缓存,当出现更新时,自动删除缓存。
timeout:设置SQL语句执行超时时间
fetchSize:控制每次从数据库读取的行数
如果SQL查询要求返回Map类型,其结果就会变成<key:value>;并且要设置谁作为key(@MapKey() ),如果返回两个参数,返回值就变成 < <key1:value1>, <key2:value2> >
3.动态查询使用for循环:
集合 传入的参数 项目 开始插入 结束插入 分隔符
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
个人感悟:
呃呃呃,不会写。。。。。。。
完成这个项目我能做的工作大多只有业务层的实现,在跟着老师慢慢完成项目的过程中,我大多都是先听老师讲完技术支持,然后自己试着将功能完成,大多都可以看着接口慢慢写出来(微信哪里一点不会)。在写的过程中也感觉到了编写接口文档是多么重要,多么困难的事情。老师在每个部分的第一节课都是讲解如何通过原型图来分析得出接口如何实现,也是在培养我的编写开发文档的能力吧,也让我知道,作为程序员代码要能写,但不能只会写代码,写文档也是很重要的能力。
我觉得跟着苍穹外卖项目,就应该大胆的去写,自己想要什么就写,不好就再改。明明是可以创造的东西,到我手里变得失去创造力了。在完成统计销量前10的接口时,遇到个问题:SQL的返回值是两个,我一下就不会了,用Map来保存返回值?我试了好久,返回值是< Map,Map> 我以为是<商品名,数量> 我想着拆,慢慢的写,试了好久还是不行,为啥?因为我钻牛角尖,不敢做。在一开始写的时候我就想是不是有个类来接收值,但我看黑马没有,我就认为不需要了,就不做了,到最后还是看视频完成的,咋完成的?创建一个类。我当时就被自己气笑了,咋能那么呆。
还有也是感觉到自己的基础有点差了,像映射,动态代理,反射这些,我都是听过、见过,但就是不知道是什么,接下对于这个项目用到的新技术也有一定的了解了,接下来就要慢慢去学习新东西和慢慢巩固基础。