day1 and day2
Swagger的介绍: 使用 Swagger Editor 编写 openapi.yaml文件,定义好 API 的整体规划(接口、数据模型)。使用 Swagger UI 嵌入项目,自动展示实时 API 文档,供前端或测试人员使用。
Swagger的使用:
Knife4j是一个为 Java项目(特别是 Spring Boot、Spring Cloud 等框架)设计的、增强型 Swagger UI 前端实现。你可以把它简单理解为 Swagger 的“超级皮肤”或“增强版”。
使用方法:加入其maven坐标,在配置类加入相关的配置,设置静态资源映射
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
</dependency>
相关配置
@Bean
public Docket docket() {
ApiInfo apiInfo = new ApiInfoBuilder()
.title("苍穹外卖项目接口文档")
.version("2.0")
.description("苍穹外卖项目接口文档")
.build();
Docket docket = new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo)
.select()
.apis(RequestHandlerSelectors.basePackage("com.sky.controller"))
.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/");
}
}
swagger常用注解:
JWT令牌的使用及其介绍
Employee employee = employeeService.login(employeeLoginDTO);
//登录成功后,生成jwt令牌
//构建载荷
Map<String, Object> claims = new HashMap<>();
claims.put(JwtClaimsConstant.EMP_ID,employee.getId());
//从配置文件读取管理员端的签名密钥(对应 HS256 算法的 secret),从配置文件读取令牌的有效期
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 令牌想象成一张「加密的身份凭证」,比如你登录某网站后,服务器不会再保存你的登录状态(不像传统 Session),而是直接给你发一个 JWT 令牌;之后你每次请求接口,都带上这个令牌,服务器只要验证令牌的合法性,就能确认你的身份,不用再查数据库 / 缓存;令牌本身包含了你的关键信息(比如用户 ID、权限),且经过加密签名,无法被篡改。
使用: 生成jwt令牌需要4步,构建header(头部声明令牌类型还有签名算法),构建payload(载荷,存放自定义声明一般把id放入进去),构建signnature(签名),最后将这三部分进行拼接生成最终的jwt字符串
JwtUtil.createJWT:这是项目自定义的 JWT 工具类(内部会自动完成: 构建 Header(指定算法,比如 HS256); 把 claims 存入 Payload,并添加 exp(过期时间)等注册声明; 用密钥对 Header+Payload 签名,生成最终的 JWT 字符串。
新增员工注意点: :首先要判断用户是否存在数据库,再对实体类employeee未赋值的进行赋值,最后还要根据ThreadLocal
关于异常处理::异常处理分为全局异常和自定义异常,无论是何种异常,都应该将异常捕获然后返回给前端
自定义业务异常:
package com.sky.common.exception;
import lombok.Getter;
/** * 自定义业务异常 * 用于处理业务逻辑中的异常(如:用户名已存在、参数校验失败等) */
@Getter // 生成getter,方便全局处理器获取属性
public class BusinessException extends RuntimeException {
// 异常状态码(对应Result的code)
private Integer code;
// 构造器1:只传异常消息(默认业务异常码400) public BusinessException(String msg) { super(msg);
// 调用父类构造器,传递异常消息 this.code = 400; }
// 构造器2:自定义状态码和消息 public BusinessException(Integer code, String msg) {
super(msg);
this.code = code; } }
系统异常:区分系统层面的异常(比如数据库连接失败、第三方接口调用失败等)
package com.sky.common.exception;
import lombok.Getter;
/**
* 自定义系统异常
* 用于处理系统层面的异常(如:数据库连接失败、Redis异常等)
*/
@Getter
public class SystemException extends RuntimeException {
private Integer code;
public SystemException(String msg) {
super(msg);
this.code = 500; // 默认系统异常码500
}
public SystemException(Integer code, String msg) {
super(msg);
this.code = code;
}
}
全局异常处理器:统一捕获所有异常,适配自定义异常和系统默认异常:
package com.sky.common.handler;
import com.sky.common.exception.BusinessException;
import com.sky.common.exception.SystemException;
import com.sky.common.result.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.sql.SQLIntegrityConstraintViolationException;
/**
* 全局异常处理器
* 捕获所有Controller层抛出的异常,统一返回格式
*/
@Slf4j // 日志注解
@RestControllerAdvice // 核心注解:全局异常处理 + 返回JSON
public class GlobalExceptionHandler {
// ========== 处理自定义异常 ==========
/**
* 处理业务异常(BusinessException)
*/
@ExceptionHandler(BusinessException.class)
public Result<?> handleBusinessException(BusinessException e) {
log.error("业务异常:{}", e.getMessage(), e); // 打印异常堆栈,方便排查
return Result.error(e.getCode(), e.getMessage());
}
/**
* 处理系统异常(SystemException)
*/
@ExceptionHandler(SystemException.class)
public Result<?> handleSystemException(SystemException e) {
log.error("系统异常:{}", e.getMessage(), e);
return Result.error(e.getCode(), e.getMessage());
}
// ========== 处理系统默认异常 ==========
/**
* 处理数据库完整性约束异常(比如唯一索引冲突)
*/
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public Result<?> handleSqlConstraintException(SQLIntegrityConstraintViolationException e) {
log.error("数据库约束异常:{}", e.getMessage(), e);
String msg = e.getMessage();
if (msg.contains("Duplicate entry")) {
// 提取重复的字段值,返回友好提示
String value = msg.split("'")[1];
return Result.error("数据重复:" + value + " 已存在!");
} else if (msg.contains("cannot be null")) {
String column = msg.split("'")[1];
return Result.error(column + " 字段不能为空!");
}
return Result.error("数据库操作失败:" + msg);
}
/**
* 处理参数校验异常(@Valid/@Validated注解触发)
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<?> handleValidException(MethodArgumentNotValidException e) {
log.error("参数校验异常:{}", e.getMessage(), e);
// 获取第一个校验失败的消息(比如:用户名不能为空)
String msg = e.getBindingResult().getFieldError().getDefaultMessage();
return Result.error(msg);
}
/**
* 兜底处理:捕获所有未明确指定的异常
*/
@ExceptionHandler(Exception.class)
public Result<?> handleAllException(Exception e) {
log.error("未知异常:{}", e.getMessage(), e); // 打印完整堆栈,方便排查
return Result.systemError("服务器内部错误,请联系管理员");
}
}
员工分页查询: :员工分页要使用到pagehelper这个插件,导入插件,然后进行使用即可
public PageInfo<User> getUserByAge(int pageNum, int pageSize, Integer age) {
// 1. 开启分页:仅对后续第一条查询生效
PageHelper.startPage(pageNum, pageSize);
// 2. 执行普通查询(PageHelper 会自动拦截并添加分页逻辑)
List<User> userList = userMapper.selectByAge(age);
// 3. 封装分页结果(包含总条数、总页数、当前页数据)
//这里也可以自定义封装数据
return new PageInfo<>(userList);
}
消息转换器: : Spring MVC 里负责前后端数据 “翻译” 的专属官,核心解决的是「HTTP 数据」和「Java 对象」之间的转换问题 —— 没有它,你就得手动写代码解析请求、拼接响应,效率极低。
举例:
用户接口
@PostMapping("/users")
public User addUser(@RequestBody User user) {
// 保存用户逻辑
return user;
}
前端发送的请求
POST /users HTTP/1.1
Content-Type: application/json
{
"name": "张三",
"age": 20
}
这个过程中,消息转换器做了两件核心事:
- 请求阶段(解码) :把 HTTP 请求体里的 JSON 字符串,转换成你代码里的
UserJava 对象(给@RequestBody用); - 响应阶段(编码) :把你代码里返回的
UserJava 对象,转换成 JSON 字符串,作为 HTTP 响应体返回给前端。
修改员工状态 :这里学到一个知识点就是动态更新,要更新某一条数据时,可以将数据封装到一个对象里面,然后用这个对象去更新数据库,好处就是提高了代码的复用性,当修改其他的字段时,也可以调用同一个方法.
修改员工信息是常规crud不介绍
day3 and day4
菜品管理:对于一些公共字段进场需要填充,这里可以使用切面类来进行
@Component: 是 Spring 最基础的注解,作用是把普通类纳入 Spring 容器管理,让类成为 Spring 的 “Bean”;告诉 Spring 容器:“这个类需要被你管理,创建它的实例并放入容器中,其他地方可以通过依赖注入(@Autowired)使用它”。
@Aspect:标识一个类为切面类,这个类中会定义:切点(Pointcut):指定要拦截哪些方法(比如所有 com.example.service 包下的方法);通知(Advice):拦截方法后要执行的逻辑(比如前置日志、异常处理、事务控制)。
@Target(ElementType.METHOD):是 Java 内置的元注解(用来修饰注解的注解),作用是限定当前自定义注解能标注在什么位置,这里表示只能在方法上
@Retention(RetentionPolicy.RUNTIME),作用是限定注解的生命周期(保留到哪个阶段),RetentionPolicy.RUNTIME 表示:这个注解在源代码编译 → 字节码文件 → 程序运行时全程保留,程序运行时可以通过反射获取到这个注解的存在。
自动填充的过程:①首先自定义注解类,用于标识哪些方法需要进行公共字段进行填充
/**
* 自定义注解,用于标识某个方法需要进行功能字段自动填充处理
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
//数据库操作类型,update,insert
OperationType value();
}
②自定义切面类,统一拦截加入自定义注解的方法,通过反射为公共字段进行赋值
@Aspect
@Component
@Slf4j
public class AutoFillAspect {
/**
* 切入点,定义切点规则:拦截 com.sky.mapper 包下带 @AutoFill 注解的方法
*/
@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
public void autoFillPointCut(){}
/**
* 前置通知
*/
@Before("autoFillPointCut()") //核心作用是指定这个前置通知要作用于哪些方法。
public void autoFill(JoinPoint joinPoint){
log.info("开始公共字段进行填充");
//获取当前被拦截的方法上的数据库类型操作
MethodSignature signature = (MethodSignature)joinPoint.getSignature();
AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);
OperationType operationType = autoFill.value(); //获取数据库操作类型字段
//获取到当前被拦截的实体对象
Object[] args = joinPoint.getArgs();
if(args == null || args.length == 0){
return;
}
//实体默认放到参数的第一个位置
Object entity = args[0];
//准备要赋值的数据
if(operationType == OperationType.INSERT){
try {
Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
//紧接着为属性赋值
setUpdateTime.invoke(entity, LocalDateTime.now());
setCreateTime.invoke(entity,LocalDateTime.now());
setCreateUser.invoke(entity, UserHolder.getUserId());
setUpdateUser.invoke(entity,UserHolder.getUserId());
} catch (Exception e) {
e.printStackTrace();
}
} else if (operationType == OperationType.UPDATE) {
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, LocalDateTime.now());
setUpdateUser.invoke(entity,UserHolder.getUserId());
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
③在mapper上加入自定义注解即可
@AutoFill(value = OperationType.INSERT)
void insert(Category category);
AOP 拦截匹配,程序怎么知道要拦截的? 当你调用 categoryMapper.insert(category) 时,你实际调用的不是 Mapper 接口的原生实现,而是 Spring 为 Mapper 生成的代理对象—— 这个代理对象会先执行 “匹配切点规则” 的逻辑,再决定是否执行原方法。
当Spring Boot项目启动时,会执行以下关键步骤: 扫描切面类:Spring 会扫描所有带 @Component + @Aspect 的类(也就是 AutoFillAspect),把它加载为 Spring Bean; 解析切点表达式:Spring 会解析 @Pointcut 里的表达式,绑定通知和切点:Spring 会把 @Before("autoFillPointCut()") 关联到上面的切点规则, “只要匹配这个切点的方法,执行前先跑 autoFill() 方法”。当代码执行 categoryMapper.insert(category) 时,实际调用的是 Spring 生成的 Mapper 代理对象,此时会触发以下步骤:代理对象拦截方法调用:代理对象接收到 insert 方法的调用请求,不会立刻执行原方法;匹配切点规则:代理对象会检查当前调用的方法是否符合阶段 1 解析的切点规则,符合规则就需要拦截然后致型前置通知再执行原方法
腾讯云上传文件: :步骤①导入腾讯云相关插件②配置文件里面配置相关的信息比如id和密钥以及存储桶名称和位置③创建腾讯云上传类来进行上传文件
@Configuration 用来标记一个类是 Spring 的配置类,替代传统 Spring 中的 XML 配置文件(比如 applicationContext.xml)。Spring 容器会扫描并解析这个类,将其中定义的 Bean 加载到容器中管理。在 @Configuration 类中想要生产Bean,必须给对应的方法加上 @Bean 注解,少了这一步 Spring 容器是不会识别并管理这个 Bean 的。
@ConditionalOnMissingBean 注解的作用,这个注解是 Spring Boot 中非常实用的条件注解,核心作用是仅当容器中不存在指定类型 / 名称的 Bean 时,才会创建当前注解标注的 Bean。
PostMapping("/upload")
@ApiOperation("文件上传")
public Result<String> upload(MultipartFile file){
log.info("文件上传到腾讯云:{}",file.getOriginalFilename());
if(file.isEmpty()){
return Result.error("文件为空上传失败");
}
//上传文件,要保证文件名字是唯一的
//获取文件的原始文件名
String originalFileName = file.getOriginalFilename();
//substring 截取文件名索引位置到字符串末尾的片段
String suffix = originalFileName.substring(originalFileName.lastIndexOf("."));
String fileName = UUID.randomUUID().toString()+suffix;
String fileAddress = null;
try {
//file.getBytes()文件全部内容读取到内存中
fileAddress = tencentOsUtil.upload(file.getBytes(), fileName);
} catch (IOException e) {
throw new RuntimeException(e);
}
return Result.success(fileAddress);
}
配置类:
@Data
@AllArgsConstructor
@NoArgsConstructor
@Slf4j
public class TencentOsUtil {
private String secretId;
private String secretKey;
private String bucketName;
private String region;
public String upload(byte[] bytes,String ObjectName) {
/**
* 构建cos上传请求
*/
//初始化cos客户端
COSCredentials credentials = new BasicCOSCredentials(secretId, secretKey);
ClientConfig clientConfig = new ClientConfig(new Region(region));
COSClient cosClient = new COSClient(credentials, clientConfig);
//上传路径
String cosFilePath = "upload/"+ObjectName;
//把byte[] 转换为字节流
ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
//上传到cos
PutObjectRequest request = new PutObjectRequest(bucketName, cosFilePath, inputStream, null);
try {
cosClient.putObject(request);
log.info("cos文件上传成功,文件路径是:{}",cosFilePath);
//拼接文件访问url
String accessUrl = String.format("https://%s.cos.%s.myqcloud.com/%s", bucketName, region, cosFilePath);
return accessUrl;
} catch (CosClientException e) {
log.error("cos客户端异常");
}finally {
if(inputStream != null){
try {
inputStream.close();
} catch (IOException e) {
log.error("关闭输入流失败",e);
}
}
if(cosClient != null){
cosClient.shutdown();//关闭cos客户端,释放连接池
}
}
return null;
}
}
/**
* 配置类,用于创建tencentUtil对象
*/
@Configuration
@Slf4j
public class TencentConfiguration {
@Bean
@ConditionalOnMissingBean
public TencentOsUtil tencentOsUtil(TencentOsProperties tencentOsProperties){
return new TencentOsUtil(tencentOsProperties.getSecretId(),tencentOsProperties.getSecretKey(), tencentOsProperties.getBucketName(),
tencentOsProperties.getRegion());
}
}
添加菜品注意点是多条口味数据批量插入,优化性能
<insert id="insertDishFlavors" parameterType="list" useGeneratedKeys="true" keyColumn="id" keyProperty="id">
insert into sky_take_out.dish_flavor(dish_id, name, value) VALUES
<foreach collection="dishFlavors" separator="," item="flavor">
(#{flavor.dishId},#{flavor.name},#{flavor.value})
</foreach>
</insert>
parameterType="list":指定传入参数的类型为 List 集合
collection="dishFlavors":指定要遍历的集合参数名称,低版本需要用参数类型
separator=",":指定遍历集合时,每个元素生成的 SQL 片段之间的分隔符
item="flavor":遍历集合时,给当前元素起的别名(后续通过 #{flavor.属性名} 取值)
对查询菜品的性能优化: 还有一种就是直接多表查询即可
//开启分页
PageHelper.startPage(dishPageQueryDTO.getPage(),dishPageQueryDTO.getPageSize());
Page<Dish> dishes = dishMapper.pageQuery(dishPageQueryDTO);
//封装
List<DishVO> dishVOS = new ArrayList<>();
Long total = dishes.getTotal();
if(total == 0){
return new PageResult(0,dishVOS);
}
List<Dish> result = dishes.getResult();
//查重category表获得分类名称,第一种就是每次都去数据库查询分类名称,这里可以性能优化,将名称先查询出来,一次,再放入map中,后面再提取即可
//1.提取所有的菜品的分类id
Set<Long> categoryIds = result.stream().map(Dish::getCategoryId).filter(Objects::nonNull).collect(Collectors.toSet());
//批量查询所有分类,转换为map
Map<Long,Category> categoryMap = new HashMap<>();
if(categoryIds!=null){
//一次拿到所有的分类
List<Category> categories = categoryMapper.selectCategoryByIds(categoryIds);
categoryMap.putAll(categories.stream().collect(Collectors.toMap(Category::getId, category -> category)));
}
dishVOS = result.stream().map(dish -> {
DishVO dishVO = new DishVO();
BeanUtils.copyProperties(dish, dishVO);
Category category = categoryMap.get(dish.getCategoryId());
dishVO.setCategoryName(category!=null?category.getName():"未知分类");
return dishVO;
}).collect(Collectors.toList());
return new PageResult(total,dishVOS);
套餐接口类也都是普通的增删改查,无知识点
day5 and day6
redis:
redis的基本操作类型:字符串,哈希,列表,集合,有序集合
使用方式:①导入Spring Data Redis的maven坐标(操作redis)②配置redis数据源③编写配置类创建RedisTemplate对象④通过RedisTemplate对象操作redis;代码不在列出,上网搜索即可.
HttpClient介绍 :可以让Java 程序像浏览器一样,主动发送 HTTP 请求(GET/POST/PUT/DELETE 等),并接收服务器返回的响应(状态码、响应体、响应头)。简单说:浏览器是 “人” 访问网页,HttpClient 是 “Java 程序” 访问网页 / 接口。在开发中会调用第三方接口,或者微服务之间的接口调用等都会使用倒HttpClient
使用过程: :导入相关的依赖包->创建 HttpClient对象->创建 Http 请求对象->调用 HttpClient的execute方法发送请求
public class HttpClientDemo {
public static void main(String[] args) throws Exception {
// 1. 创建 HttpClient 实例(相当于打开一个浏览器)
CloseableHttpClient httpClient = HttpClients.createDefault();
// 2. 创建 GET 请求(相当于在地址栏输入网址)
HttpGet httpGet = new HttpGet("https://www.baidu.com");
// 3. 发送请求,获取响应(相当于点击回车,接收网页内容)
CloseableHttpResponse response = httpClient.execute(httpGet);
// 4. 解析响应(获取状态码、响应体)
System.out.println("状态码:" + response.getStatusLine().getStatusCode());
String responseBody = EntityUtils.toString(response.getEntity(), "UTF-8");
System.out.println("响应内容:" + responseBody);
// 5. 关闭资源(相当于关闭浏览器)
response.close();
httpClient.close();
}
}
文件配置 :传统 Java 项目常用.properties文件做配置,格式是key=value,层级嵌套时可读性差;而 YML 使用缩进(空格)表示层级,结构更清晰,支持列表、嵌套对象等复杂结构
@ConfigurationProperties(prefix = "sky.wechat"):告诉 Spring,去配置文件中读取以sky.wechat为前缀的所有配置项,自动映射到这个类的属性上,并把这些配置值封装成一个可复用的 Java 对象,方便在项目其他地方调用。
@Component
@ConfigurationProperties(prefix = "sky.wechat")
@Data
public class WeChatProperties {
private String appid; //小程序的appid
private String secret; //小程序的秘钥
private String mchid; //商户号
private String mchSerialNo; //商户API证书的证书序列号
private String privateKeyFilePath; //商户私钥文件
private String apiV3Key; //证书解密的密钥
private String weChatPayCertFilePath; //平台证书
private String notifyUrl; //支付成功的回调地址
private String refundNotifyUrl; //退款成功的回调地址
}
day7 and day8 and day9 进入redis学习
Redis 中存储业务数据时,字段(Key)该如何规范命名,以及不同场景该选什么数据类型:
命名格式: 业务模块:数据类型:唯一标识[:子标识]
具体事例:
redis基本数据类型使用场景:
redis缓存清理 :当管理端修改增加某些数据时,需要同步的对redis缓存里面的数据进行修改
SpringCache的介绍和使用 :Spring Cache 是 Spring 框架提供的缓存抽象层,它不直接实现缓存,而是定义了一套统一的缓存注解和接口,底层可以对接 Redis、Ehcache、Caffeine 等具体缓存实现。
价值: 解耦缓存逻辑和业务逻辑,只需加注解就能给方法添加缓存能力,无需手动编写redisTemplate.opsForValue().set()等代码;
常用注解:
例子:
@RestController
@RequestMapping("/user")
@Slf4j
@EnableCaching
public class UserController {
@Autowired
private UserMapper userMapper;
@PostMapping
@CachePut(cacheNames ="userCache",key = "#user.id") //如果用springCache缓存数据,那么key的生成 cacheNames::key
//@CachePut(cacheNames ="userCache",key = "#result.id") //对象导航
//@CachePut(cacheNames ="userCache",key = "#a0.id") //取方法的第一个参数,#p0.id也可以,#root.args[0].id也可以
public User save(@RequestBody User user){
userMapper.insert(user);
return user;
}
@DeleteMapping
@CacheEvict(cacheNames = "userCache",key = "#id")
public void deleteById(Long id){
userMapper.deleteById(id);
}
@DeleteMapping("/delAll")
@CacheEvict(cacheNames = "userCache",allEntries = true) //删除该缓存下的所有键值对
public void deleteAll(){
userMapper.deleteAll();
}
@GetMapping
@Cacheable(cacheNames = "userCache",key = "#id")
public User getById(Long id){
User user = userMapper.getById(id);
return user;
}
@RestController
public class UserController {
@PostMapping
@CachePut(cacheNames = "userCache", key = "#result.id")
public User save(@RequestBody User user) {
userMapper.insert(user); // 核心业务逻辑
return user;
}
}
spring Cache注解的执行原理: 例如上面代码
①Spring 启动时,发现这个类有@RestController(被 Spring 管理),且方法有@CachePut注解;②Spring 会自动为UserController创建一个代理对象(JDK/CGLIB 代理);③当你调用save方法时,实际调用的是代理对象的 save 方法,而非原对象的方法;④代理对象的逻辑:先调用原对象的save方法(执行插入数据库、主键回填),拿到返回的user对象后,解析@CachePut注解的规则(cacheNames=userCache、key=#result.id),把user对象存入 Redis(Key:userCache::用户ID),最后返回user对象给调用者。如果没有代理对象,步骤 4 的缓存逻辑就无法 “插入” 到方法执行流程中,注解也就完全失效。
代理对象介绍: 代理对象就像业务对象的 “代言人” :你原本要直接调用UserController.save()方法(目标对象),但 Spring 会创建一个代理对象包裹原对象,你实际调用的是代理对象的save()方法;代理对象会在调用原方法前后,自动执行额外逻辑(如缓存、事务、日志),最后再调用原对象的方法。
代理的两种类型:
Spring 自动创建代理对象: ①目标对象被 Spring 管理(加
@Controller/@Service/@Component)②方法上添加 Spring 注解(如@CachePut、@Transactional)。
// 1. 目标对象(被Spring管理)
@RestController // 关键:Spring会为这个类创建代理对象
public class UserController {
@Autowired
private UserMapper userMapper;
// 2. 标注注解,Spring代理会自动处理缓存逻辑
@PostMapping
@CachePut(cacheNames = "userCache", key = "#result.id")
public User save(@RequestBody User user) {
userMapper.insert(user); // 核心业务逻辑
return user;
}
}
// 3. 调用方式(通过Spring容器获取代理对象)
@Service
public class TestService {
@Autowired
private UserController userController; // 注入的是代理对象,而非原对象
public void test() {
// 实际调用的是代理对象的save方法,缓存逻辑自动执行
userController.save(new User("张三", 25));
}
}
也可以手动创建代理对象: 分为上面的接口和无接口场景
注意: 内部方法调用、手动 new 对象会导致代理失效,注解(如@CachePut)无法生效。
所有返回 List 类型的方法,无论是否查到数据,都会返回一个有效的 List 对象(不会返回 null);只是查到数据时列表有元素,查不到时列表为空(元素数量为 0)。
当sql语句需要二选一时,用choose不用if,choose只会执行第一个选中的,其他的when都会忽略掉
<delete id="deleteCarts">
delete from sky_take_out.shopping_cart where user_id = #{userId}
<choose>
<when test="shoppingCartDTO.dishId !=null"> and dish_id = #{shoppingCartDTO.dishId}</when>
<when test="shoppingCartDTO.setmealId !=null">and setmeal_id = #{shoppingCartDTO.setmealId} </when>
<otherwise>
and 1=0 <!-- 让SQL永远不执行,或抛异常 -->
</otherwise>
</choose>
</delete>
微信支付时序图:
String.join("",orderDishList); 将数组用""分隔符拼接到一起
day10 and # day11 and dya 12
JSON.toJSONString()可以将数据转化为json格式
Spring task: 可以自己定时执行某一段代码
应用场景 :清理过期数据,清理用户上传的临时课件 / 作业文件(比如 24 小时未提交的草稿文件),清理 Redis 中过期的登录验证码、临时课程预览链接。订单超时,定时数据统计等
使用方法::加入Maven坐标->在启动类上添加@EnableScheduling注解,然后创建任务,在任务上加入@Scheduled(fixedRate = 5000)即可
@Component
@Slf4j
public class OrderTask {
@Autowired
private OrderMapper orderMapper;
/**
* 处理超时订单的方法
*/
@Scheduled(fixedRate = 5000)
public void processTimeoutOrder(){
log.info("定时处理超时订单:{}", LocalDateTime.now());
//处理代付款状态的订单
List<Orders> ordersList = orderMapper.getByStatusAndOrderTimeLT(Orders.PENDING_PAYMENT,LocalDateTime.now().plusMinutes(-15));
if(ordersList !=null && ordersList.size() >0){
for(Orders orders:ordersList){
//将订单的状态修改成取消,并且要进行退款
orders.setStatus(Orders.CANCELLED);
orders.setCancelReason("订单超时自动取消");
orders.setCancelTime(LocalDateTime.now());
orderMapper.update(orders);
}
}else{
System.out.println("没有超时订单");
}
}
/**
* 处理派送中的订单
*
*/
@Scheduled(cron ="20 * * * * ?" )
public void processDeliveryOrder(){
log.info("处理一直在派送中的订单:{}",LocalDateTime.now());
List<Orders> ordersList = orderMapper.getByStatusAndOrderTimeLT(Orders.DELIVERY_IN_PROGRESS, LocalDateTime.now().plusMinutes(-60));
if(ordersList !=null && ordersList.size() >0){
for(Orders orders:ordersList){
//将订单的状态修改成已完成
orders.setStatus(Orders.COMPLETED);
orderMapper.update(orders);
}
}else{
System.out.println("没有一直派送的订单");
}
}
}
WebSocket介绍:WebSocket 是一种全双工、长连接的网络通信协议(协议标识:ws:///wss://),能在客户端(浏览器)和服务器之间建立持续的双向通信通道,实现服务器主动向客户端推送数据,无需客户端频繁轮询。底层和http一样都是基于TCP连接
应用场景: :在线聊天,实时消息通知
使用方式:①配置类
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfiguration {
// 注册ServerEndpointExporter,让Spring识别@ServerEndpoint注解
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
②:定义 WebSocket 端点类
@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();
}
}
}
}
使用例子:
@Override
public void reminder(Long id) {
Orders ordersDB = orderMapper.queryOrder(id);
if(ordersDB == null ){
throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);
}
//进行催单
Map map = new HashMap();
map.put("type",2);//1来单提醒,2客户催单
map.put("orderId:",id);
map.put("content","订单号:"+ordersDB.getNumber());
String json = JSON.toJSONString(map);
webSocketServer.sendToAllClient(json);
}
Apache Echarts :用于绘制图表数据,前端内容,官网可以查询资料
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin 这是一个用于日格式化的注解,将传过来的对象转换成这个格式
StringUtils.join(localDateList, ","):将列表转换为字符串
Apche POI:用于操作excel文件,下面有代码例子,一般来说对于导出文件,需要自己先创建一个模板文件然后导出即可.导出过程: 创建获取()excel文件->创建(获取)标签页sheet->创建(获取)行然后进行数据填充.
HttpServletResponse: 是 Servlet 规范中定义的核心接口,中文译作「HTTP 响应对象」,代表服务器向客户端(浏览器 / 客户端程序)返回的所有内容(包括响应头、响应体、状态码等)。
public class POITest {
//通过poi创建excel文件并且写入文件内容
//poi创建excel文件并且写入文件内容
public static void write() throws IOException {
//内存中创建excel文件
XSSFWorkbook excel = new XSSFWorkbook();
//文件中创建一个sheet页
XSSFSheet sheet = excel.createSheet("info");
//在sheet中创建行对象,rownum编号从0开始
XSSFRow row = sheet.createRow(1);
//创建单元格并且写入文件内容
row.createCell(1).setCellValue("姓名");
row.createCell(2).setCellValue("城市");
//创建一个新行
row = sheet.createRow(2);
row.createCell(1).setCellValue("张三");
row.createCell(2).setCellValue("北京");
row = sheet.createRow(3);
row.createCell(1).setCellValue("李斯");
row.createCell(2).setCellValue("南京");
FileOutputStream out = new FileOutputStream(new File("D:\\info.xlsx"));
excel.write(out);
//关闭资源
out.close();
excel.close();
}
//poi读取excel文件
public static void read() throws Exception{
//读取磁盘上已经存在的文件
String filePath = "D:\\info.xlsx";
FileInputStream in = new FileInputStream(filePath);
XSSFWorkbook excel = new XSSFWorkbook(in);
//读取第一个sheet页
XSSFSheet sheet = excel.getSheetAt(0);
//获取最后一行的行号
int lastRowNum = sheet.getLastRowNum();
for (int i = 1; i <= lastRowNum; i++) {
//获得某一行
XSSFRow row = sheet.getRow(i);
//获得每一行的单元格对象
String cellValue1 = row.getCell(1).getStringCellValue();
String cellValue2 = row.getCell(2).getStringCellValue();
System.out.println(cellValue1+" "+cellValue2);
}
//关闭资源
excel.close();
in.close();
}
@Override
public void exportBusinessData(HttpServletResponse response) {
//1.查询数据库,获取营业数据
LocalDate begin = LocalDate.now().minusDays(30);
LocalDate end = LocalDate.now().minusDays(1);
BusinessDataVO businessDataVO = workspaceService.getBusinessData(begin,end );
//2.通过POI将数据写入到excel文件中
InputStream in = this.getClass().getClassLoader().getResourceAsStream("template/运营数据报表模板.xlsx");
try {
XSSFWorkbook excel = new XSSFWorkbook(in);
//对应模板填充数据
//获取标签页
XSSFSheet sheet = excel.getSheet("Sheet1");
//设置行数据
sheet.getRow(1).getCell(1).setCellValue("时间:"+LocalDate.now().minusMonths(1)+"----"+LocalDate.now().minusDays(1));
sheet.getRow(3).getCell(2).setCellValue(businessDataVO.getTurnover());
sheet.getRow(3).getCell(4).setCellValue(businessDataVO.getOrderCompletionRate());
sheet.getRow(3).getCell(6).setCellValue(businessDataVO.getNewUsers());
sheet.getRow(4).getCell(3).setCellValue(businessDataVO.getValidOrderCount());
sheet.getRow(4).getCell(2).setCellValue(businessDataVO.getUnitPrice());
//填充明细数据
for (int i = 0; i < 30; i++) {
LocalDate date = begin.plusDays(i);
BusinessDataVO businessData = workspaceService.getBusinessData(date,date);
//从第8行开始填充
sheet.getRow(7+i).getCell(1).setCellValue(date.toString());
sheet.getRow(7+i).getCell(2).setCellValue(businessDataVO.getTurnover());
sheet.getRow(7+i).getCell(3).setCellValue(businessData.getValidOrderCount());
sheet.getRow(7+i).getCell(4).setCellValue(businessData.getOrderCompletionRate());
sheet.getRow(7+i).getCell(5).setCellValue(businessData.getUnitPrice());
sheet.getRow(7+i).getCell(6).setCellValue(businessData.getNewUsers());
}
//3.通过输出流将excel文件下载到浏览器
ServletOutputStream out = response.getOutputStream();
excel.write(out);
out.close();
excel.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
}