项目-cqwm
前端环境
-
nginx反向代理
-
nginx负责把浏览器请求转到tomcat
-
前端nginx.exe双击打开就行
-
想关闭
任务管理器或者cmd taskkill /f /t /im nginx.exe
-
请求 http://localhost:8080/admin就会反向代理 http://localhost:80/api
jwt令牌
-
相当于一个证件,以后改用户每次请求都要经过验证
package com.sky.utils; import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import java.nio.charset.StandardCharsets; import java.util.Date; import java.util.Map; 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 parseJWT(String secretKey, String token) { // 得到DefaultJwtParser Claims claims = Jwts.parser() // 设置签名的秘钥 .setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8)) // 设置需要解析的jwt .parseClaimsJws(token).getBody(); return claims; } }
md5加密
-
数据库先改,md5加密工具
-
方法验证加上
password = DigestUtils.md5DigestAsHex(password.getBytes());
swagger接口文档
- 生成文档
- 测试
-
使用方法
-
导入坐标
-
knife4j 是java mvc框架集成swagger生成api文档增强解决方案
<dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-spring-boot-starter</artifactId> </dependency>
-
配置类,配置属性
/** * 通过knife4j生成接口文档 * @return */ @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; }
-
配置类,配置静态资源过滤,防止访问swagger静态资源被以为是请求
/** * 设置静态资源映射 * @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/"); }
@Slf4j
-
开启日志,可以控制台输出了
log.info("开始注册自定义拦截器...");
拷贝属性
-
DTO到普通数据库实体,要求属性名相同
Employee employee = new Employee(); // 拷贝属性 BeanUtils.copyProperties(employeeDTO,employee);
时间
//@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
// 创建修改时间
employee.setCreateTime(LocalDateTime.now());
employee.setUpdateTime(LocalDateTime.now());
时间戳
- 1970.1.1 0:0:0到现在的ms数
- 可以拿来做订单号
System.currentTimeMillis();
sql提示插件
- mybatis-x
swagger设置jwt令牌
-
所有controller请求前 验证jwt
package com.sky.interceptor; import com.sky.constant.JwtClaimsConstant; import com.sky.properties.JwtProperties; import com.sky.utils.JwtUtil; import io.jsonwebtoken.Claims; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * jwt令牌校验的拦截器 */ @Component @Slf4j public class JwtTokenAdminInterceptor implements HandlerInterceptor { @Autowired private JwtProperties jwtProperties; /** * 校验jwt在所有controller请求前 * * @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()); log.info("当前员工id:", empId); //3、通过,放行 return true; } catch (Exception ex) { //4、不通过,响应401状态码 response.setStatus(401); return false; } } }
-
从登录获取jwt令牌,叫token,在yml资源里面设置名字了
添加到swagger,之后请求头会加上这个,验证才能通过
处理注册时用户登录名重复500错
-
SQLIntegrityConstraintViolationException: Duplicate entry '666' for key 'idx_username'
-
定义全局异常处理器
@ExceptionHandler public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){ // SQLIntegrityConstraintViolationException // Duplicate entry '666' for key 'idx_username' log.error("异常信息:{}", ex.getMessage()); if (ex.getMessage().contains("Duplicate entry")){ String[] msg = ex.getMessage().split(" "); log.info(Arrays.toString(msg)); return Result.error(msg[2] + MessageConstant.ALREADY_EXISTS); }else return Result.error(MessageConstant.UNKNOWN_ERROR); }
log占位符{}
log.info("jwt校验:{}", token);// token实际值会替代{}
线程空间
-
每次请求都是新的线程,线程中可以存有线程空间实现当前线程变量共用
-
threadLocal 存取删
-
定义包装工具类BaseContext和对应方法
package com.sky.context; 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(); } }
-
jwt验证时存入空间
String token = request.getHeader(jwtProperties.getAdminTokenName()); log.info("jwt校验:{}", token); Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token); Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString()); log.info("当前员工id:{}", empId); // 在线程空间存入id BaseContext.setCurrentId(empId); //3、通过,放行 return true;
-
service类取出
// 创建修改人id // TODO 用线程空间实现获取修改人id,先解析再放入线程空间(已完成) employee.setCreateUser(BaseContext.getCurrentId()); employee.setUpdateUser(BaseContext.getCurrentId());
JWT工作流程原理
- 用户提交用户名密码认证,成功生成加密JWT 前端保存JWT,每次前端请求都在头携带JWT,后端在执行controller前拦截验证JWT,通过才执行,展示数据,不通过就返回错误信息
查询封装
- DTO 用户传给后台的参数数据
- VO 后台返回给前端展示的数据
concat
mqsql函数
<select id="pageQuery" resultType="com.sky.entity.Employee">
select * from employee
<where>
<if test="name!=null and name!=''">
and name like concat('%','#{name}','%')
</if>
</where>
order by create_time desc
</select>
pagehelper
-
导入坐标
-
serviceimpl使用,本质也是用threadlocal 存数据,然后动态拦截拼接
@Override public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) { PageHelper.startPage(employeePageQueryDTO.getPage(),employeePageQueryDTO.getPageSize()); Page<Employee> page = employeeMapper.pageQuery(employeePageQueryDTO); Long total = page.getTotal(); List<Employee> records = page.getResult(); return new PageResult(total,records); }
实现代码流程
-
看产品原型,定义好接口请求方式,接口传入参数,返回参数
例:分页查询 需要传入PageQueryDTO
package com.sky.dto; import lombok.Data; import java.io.Serializable; @Data public class EmployeePageQueryDTO implements Serializable { //员工姓名 private String name; //页码 private int page; //每页显示记录数 private int pageSize; }
统一返回Result
package com.sky.result; import lombok.Data; import java.io.Serializable; /** * 后端统一返回结果 * @param <T> */ @Data public class Result<T> implements Serializable { private Integer code; //编码:1成功,0和其它数字为失败 private String msg; //错误信息 private T data; //数据 public static <T> Result<T> success() { Result<T> result = new Result<T>(); result.code = 1; return result; } public static <T> Result<T> success(T object) { Result<T> result = new Result<T>(); result.data = object; result.code = 1; return result; } public static <T> Result<T> error(String msg) { Result result = new Result(); result.msg = msg; result.code = 0; return result; } }
此处返回数据,查询一般才有,分页返回数据统一为pageresult
package com.sky.result; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; import java.util.List; /** * 封装分页查询结果 */ @Data @AllArgsConstructor @NoArgsConstructor public class PageResult implements Serializable { private long total; //总记录数 private List records; //当前页数据集合 }
-
定义完接口后,根据接口定义,从controller开始编写,返回result,传入dto,
controller调用service,业务在service实现
@GetMapping("/page") @ApiOperation("员工分页查询") public Result<PageResult> page(EmployeePageQueryDTO employeePageQueryDTO){ log.info("打印DTO:{}",employeePageQueryDTO); log.info("当前时间:{}",LocalDateTime.now()); PageResult pageResult = employeeService.pageQuery(employeePageQueryDTO); return Result.success(pageResult); }
-
service先写接口,再实现类编写具体
serviceimpl调用mapper,mapper实现具体sql,service想办法实现返回类型
@Override public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) { PageHelper.startPage(employeePageQueryDTO.getPage(),employeePageQueryDTO.getPageSize()); Page<Employee> page = employeeMapper.pageQuery(employeePageQueryDTO); Long total = page.getTotal(); List<Employee> records = page.getResult(); return new PageResult(total,records); }
-
mapper具体实现sql,动态sql要在xml编写
Page<Employee> pageQuery(EmployeePageQueryDTO employeePageQueryDTO);
<?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="com.sky.mapper.EmployeeMapper"> <select id="pageQuery" resultType="com.sky.entity.Employee"> select * from employee <where> <if test="name!=null and name!=''"> and name like concat('%',#{name},'%') </if> </where> order by create_time desc </select> </mapper>
json和@RequestBody和get post
关键区别
方式 | 适用场景 | HTTP方法 | Content-Type | 参数位置 |
---|---|---|---|---|
@RequestBody |
复杂数据(如JSON) | POST/PUT | application/json |
请求体 |
查询参数或表单数据 | 简单键值对 | GET | x-www-form-urlencoded |
URL查询字符串 |
错误示例分析
- 错误情况:在GET方法中使用
@RequestBody
,客户端发送JSON请求体。 - 结果:服务器无法解析请求体,抛出异常,因为GET请求的请求体通常被忽略。
总结
- 需要传递JSON数据 → 使用
@PostMapping
+@RequestBody
。 - 保持GET请求 → 移除
@RequestBody
,通过查询字符串传参。
JWT令牌过期
sky:
jwt:
# 设置jwt签名加密时使用的秘钥
admin-secret-key: itcast
# 设置jwt过期时间 7200s=2h
admin-ttl: 7200000
# 设置前端传递过来的令牌名称
admin-token-name: token
package com.sky.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
// TODO 通过yml配置文件加载
@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;
}
注意sql函数concat
{name}不能用单引号包
<select id="pageQuery" resultType="com.sky.entity.Employee">
select * from employee
<where>
<if test="name!=null and name!=''">
and name like concat('%',#{name},'%')
</if>
</where>
order by create_time desc
</select>
实现日期合理显示
-
注解,直接改json显示格式(方便,要改的多了不推荐)
// @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime createTime; //@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime updateTime;
-
定义消息转换器,加入springmvc官方转换器集合,消息转换器内加上对象转换器(规则)
/** * 扩展springmvc的消息转化器 * @param converters */ @Override protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) { // 创建一个消息转换器对象 MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); // JacksonObjectMapper为规则 对象转换器,可以将java对象序列化为json converter.setObjectMapper(new JacksonObjectMapper()); // 将消息转化器装入springmvc自带的转化器集合list 记得设置为第一个index=0,才能有效使用,顺序生效 converters.add(0,converter); }
git提交更新
选择勾
- 无脑提交并推送
Restful风格
-
/status/0 为restful风格 参数记得加@PathVariable
? id = 11 普通地址栏传参
@PostMapping("/status/{status}") @ApiOperation("员工账号启用禁用") public Result startOrStop(@PathVariable Integer status,Long id){ log.info("启用禁用员工账户:{},{}",status,id); employeeService.startOrStop(status,id); return Result.success(); }
请求和增删改查的对应
-
get 查询操作
有返回,不用携带
-
post 增加操作
无返回,要json携带
-
put 修改操作
无返回,要json携带
-
delete 删除操作
无返回,地址栏传参普通携带
新初始化对象方式build
-
Employee 类上加@Builder注解
-
使用
Employee employee = Employee.builder() .id(id) .status(status) .build();java
动态sql注意点
-
set标签内最后记得加,逗号分割
-
sql语句 update_time = #{updateTime},
注意前面后面不一定一样,前面是sql数据库,后面是实体类,要对应
<update id="update" parameterType="employee"> update employee <set> <if test="name!=null">name = #{name},</if> <if test="username!=null">username = #{username},</if> <if test="password!=null">password = #{password},</if> <if test="phone!=null">phone = #{phone},</if> <if test="sex!=null">sex = #{sex},</if> <if test="idNumber!=null">id_number = #{idNumber},</if> <if test="status!=null">status = #{status},</if> <if test="updateTime!=null">update_time = #{updateTime},</if> <if test="updateUser!=null">update_user = #{updateUser},</if> </set> where id = #{id}; </update>
公共字段填充(反射)
-
定义注解
package com.sky.annotation; import com.sky.enumeration.OperationType; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 自定义标识注解 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface AutoFill { // 标识是insert还是update OperationType value(); }
-
定义切入点和切面
package com.sky.aspect; import com.sky.annotation.AutoFill; import com.sky.constant.AutoFillConstant; import com.sky.context.BaseContext; import com.sky.enumeration.OperationType; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.Signature; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.stereotype.Component; import java.lang.reflect.Method; import java.time.LocalDateTime; /** * 自定义切面类 */ @Component @Slf4j @Aspect public class AutoFillAspect { /** * 切入点 3个*为返回类型,类名,方法名 */ @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)") public void autoFillPointCut(){ } /** * 切入面,要切什么,在哪切 */ @Before("autoFillPointCut()") public void autoFill(JoinPoint joinPoint){ log.info("切入方法:{}",joinPoint); // 获取操作类型 insert update MethodSignature signature = (MethodSignature) joinPoint.getSignature(); log.info("切入signature:{}",signature); AutoFill annotation = signature.getMethod().getAnnotation(AutoFill.class); OperationType type = annotation.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 (type==OperationType.INSERT){ try { Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class); Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_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); setCreateTime.invoke(entity,now); setUpdateTime.invoke(entity,now); setCreateUser.invoke(entity,currentId); setUpdateUser.invoke(entity,currentId); } catch (Exception e) { throw new RuntimeException(e); } }else if (type==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,now); setUpdateUser.invoke(entity,currentId); } catch (Exception e) { throw new RuntimeException(e); } } } }
-
在mapper对应修改和删除方法上加注解
@AutoFill(value = OperationType.UPDATE) void update(Employee employee); @AutoFill(value = OperationType.INSERT) void save(Employee employee); @AutoFill(value = OperationType.INSERT) void insert(Category category); @AutoFill(value = OperationType.UPDATE) void update(Category category);
aliosss配置
-
application-dev.yml
具体配置
sky: datasource: driver-class-name: com.mysql.cj.jdbc.Driver host: localhost port: 3306 database: sky_take_out username: root password: 123456 alioss: endpoint: oss-cn-beijing.aliyuncs.com access-key-id: LTAI5tS3dSuNTc2F2H4azLCM access-key-secret: 2JdiyCpCXEdRmX27TAi3wu6B4MmxNb bucket-name: sky---heima
-
主文件application.yml引用方法
${sky.alioss.endpoint}
spring: profiles: active: dev sky: jwt: # 设置jwt签名加密时使用的秘钥 admin-secret-key: itcast # 设置jwt过期时间 7200s=2h admin-ttl: 7200000 # 设置前端传递过来的令牌名称 admin-token-name: token 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}
-
aliossproperties
引用配置
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; }
MultipartFile
-
简化操作,直接接收文件,要求前后端名字一致file
-
MultipartFile
简化了 Spring 中文件上传的操作,使得开发者可以专注于业务逻辑(如文件存储、处理),而无需关注底层 HTTP 协议的细节。通过合理配置和异常处理,可以构建安全可靠的文件上传功能。 -
MultipartFile 是 Spring Framework 中用于处理 HTTP 文件上传的核心接口。在您提供的代码中,它被用来接收客户端通过
multipart/form-data
格式上传的文件。以下是详细解释: -
核心概念
- HTTP 文件上传 当客户端通过表单上传文件时,通常会使用
enctype="multipart/form-data"
。此时文件内容会以多部分(multipart)形式传输到服务器。 - Spring 的封装 Spring 通过
MultipartFile
接口抽象了这一过程,开发者无需直接解析复杂的 HTTP 请求体,而是通过简单的 API 操作文件。
alioss上传
-
导入依赖
<dependency> <groupId>com.aliyun.oss</groupId> <artifactId>aliyun-sdk-oss</artifactId> <version>${aliyun.sdk.oss}</version> </dependency>
-
aliosss配置
yml
-
具体实现 工具类
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(); } }
-
配置类 初始化aliossutil
package com.sky.config; import com.sky.properties.AliOssProperties; import com.sky.utils.AliOssUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * 配置类,用于创建AliOssUtil对象的属性初始化 */ @Configuration @Slf4j public class OssConfiguration { // 项目启动,创建AliOssUtil对象,交给spring管理 @Bean @ConditionalOnMissingBean public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties){ log.info("创建阿里云文件上传工具类对象:{}",aliOssProperties); return new AliOssUtil(aliOssProperties.getEndpoint(), aliOssProperties.getAccessKeyId(), aliOssProperties.getAccessKeySecret(), aliOssProperties.getBucketName()); } }
-
调用
package com.sky.controller.admin; import com.sky.constant.MessageConstant; import com.sky.result.Result; import com.sky.utils.AliOssUtil; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.util.UUID; /** * 通用接口 */ @RestController @RequestMapping("/admin/common") @Api(tags = "通用接口") @Slf4j public class CommonController { @Autowired private AliOssUtil aliOssUtil; /** * 文件上传 * @param file * @return */ @PostMapping("/upload") @ApiOperation("文件上传") public Result<String> upload(MultipartFile file){ log.info("文件上传:{}",file); try { // 获取原文件名 String originalFilename = file.getOriginalFilename(); // 截取后缀 .png String extension = originalFilename.substring(originalFilename.lastIndexOf(".")); // 构造新文件名 String objectName = UUID.randomUUID().toString() + extension; String filePath = aliOssUtil.upload(file.getBytes(), objectName); return Result.success(filePath); } catch (IOException e) { log.error("上传失败:{}",e.getMessage()); } return Result.error(MessageConstant.UPLOAD_FAILED); } }
截取文件字符串
// 截取后缀 .png
String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
UUID随机命名,防止文件名字重复
// 构造新文件名
String objectName = UUID.randomUUID().toString();
操作多表开启事务包装原子性
-
启动类加
@EnableTransactionManagement
package com.sky; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.transaction.annotation.EnableTransactionManagement; @SpringBootApplication @EnableTransactionManagement //开启注解方式的事务管理 @Slf4j public class SkyApplication { public static void main(String[] args) { SpringApplication.run(SkyApplication.class, args); log.info("server started"); } }
-
操作多表的serviceimpl方法加
@Transactional 开启事务管理
@Override // TODO 开启事务管理 @Transactional public void saveWithFlavor(DishDTO dishDTO) { // 1 增加1条dish菜品数据 Dish dish = new Dish(); BeanUtils.copyProperties(dishDTO,dish); dishMapper.insert(dish); // 获取主键值给给口味表 Long dishId = dish.getId(); // 2 增加n条flavor口味数据 List<DishFlavor> flavors = dishDTO.getFlavors(); if (flavors!=null && flavors.size()>0){ flavors.forEach(dishFlavor -> { dishFlavor.setDishId(dishId); }); dishFlavorMapper.insertBatch(flavors); } }
实现流程
-
写controller
加四个注解 创建service controller
controller调用service
@RestController @RequestMapping("/admin/dish") @Slf4j @Api(tags = "菜品相关接口")
-
写service接口
-
写serviceimpl实现类
加@service接口,实现自动装配
service调用mapper
-
写对应mapper接口
记得@Mapper接口,实现mapper自动装配
简单sql就用注解,复杂需要xml文件
xml记得改命名空间namespace
如果有公共字段记得@AutoFill(value = OperationType.INSERT)
获取自增id
-
useGeneratedKeys 表示启用数据库的「自动生成主键」功能(例如 MySQL 的
AUTO_INCREMENT
、PostgreSQL 的SERIAL
)。 设置为true
后,MyBatis 会通过 JDBC 获取数据库生成的主键值。 -
keyProperty ="id"把数据库生成的主键赋值给 实体类的id,此时实体类对象才有id
<insert id="insert" parameterType="dish" useGeneratedKeys="true" keyProperty="id"> insert into dish (name, category_id, price, image, description, status, create_time, update_time, create_user, update_user) VALUES (#{name},#{categoryId},#{price},#{image},#{description},#{status},#{createTime},#{updateTime},#{createUser},#{updateUser}) </insert>
-
再通过getid获取
Dish dish = new Dish(); BeanUtils.copyProperties(dishDTO,dish); dishMapper.insert(dish); // 获取主键值给给口味表 Long dishId = dish.getId(); // 2 增加n条flavor口味数据 List<DishFlavor> flavors = dishDTO.getFlavors(); if (flavors!=null && flavors.size()>0){ flavors.forEach(dishFlavor -> { dishFlavor.setDishId(dishId); }); dishFlavorMapper.insertBatch(flavors); }
foreach
-
跟增强for差不多
List<DishFlavor> flavors = dishDTO.getFlavors(); if (flavors!=null && flavors.size()>0){ flavors.forEach(dishFlavor -> { dishFlavor.setDishId(dishId); }); dishFlavorMapper.insertBatch(flavors); }
动态sql遍历
-
传入集合
void insertBatch(List<DishFlavor> flavors);
-
foreach动态sql
collection 参数名
item 每一项的遍历值
separator 分割符号
<insert id="insertBatch"> insert into dish_flavor (dish_id, name, value) VALUES <foreach collection="flavors" item="df" separator=","> (#{df.dishId},#{df.name},#{df.value}) </foreach> </insert>
实体类Dish,前端传值DishDTO,后端返回DishVO
-
实体类,与数据库完全一一对应
-
DTO,数据传输对象,封装前端传参,一般传后端需要的,用户输入的,后端可以自动生成的不传(时间,操作人)
还有就是其他关联表的数据,类似菜品口味等
封装数据库返回结果
-
VO,视图显示对象,后端返回给前端,传前端需要的,一般是多表关联的数据,连表
sql连表查询
-
left join
-
right join
-
inner join
-
实际sql
<select id="pageQuery" resultType="com.sky.vo.DishVO"> select d.*,c.name as categoryName from dish d left join category c on d.category_id = c.id <where> <if test="name!=null">and d.name like concat('%',#{name},'%')</if> <if test="categoryId!=null">and category_id = #{categoryId}</if> <if test="status!=null">and d.status = #{status}</if> </where> order by d.create_time desc </select>
RequestParam
@RequestParam
是 Spring MVC 框架中的一个注解,用于从 HTTP 请求中提取请求参数(如 URL 中的查询参数或表单提交的参数),并将其绑定到控制器(Controller)方法的参数上。它是处理客户端请求参数的常用工具,尤其在 RESTful API 或 Web 应用开发中非常实用。
主要用途
-
绑定请求参数到方法参数 将 HTTP 请求中的参数(如
?name=John&age=20
)映射到 Controller 方法的参数上。 -
处理可选参数 通过设置
required
属性,可以控制参数是否为必传。 -
设置默认值 当请求未提供参数时,可以通过
defaultValue
属性指定默认值。 -
处理多值参数 支持将多个同名参数(如复选框数据)绑定到集合或数组。
可以把前端ids=1,2,3绑定到ids集合
@DeleteMapping @ApiOperation("批量删除菜品") public Result delete(@RequestParam List<Long> ids){ log.info("批量删除菜品对应id:{}",ids); dishService.deleteBatch(ids); return Result.success(); }
全局异常处理
- BaseException ex代表拦截处理抛出这类异常,包括BaseException 子类
package com.sky.handler;
import com.sky.constant.MessageConstant;
import com.sky.exception.BaseException;
import com.sky.result.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.sql.SQLIntegrityConstraintViolationException;
import java.util.Arrays;
/**
* 全局异常处理器,处理项目中抛出的业务异常
*/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 捕获业务异常
* @param ex
* @return
*/
@ExceptionHandler
public Result exceptionHandler(BaseException ex){
log.error("异常信息:{}", ex.getMessage());
return Result.error(ex.getMessage());
}
@ExceptionHandler
public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){
// SQLIntegrityConstraintViolationException
// Duplicate entry '666' for key 'idx_username'
log.error("异常信息:{}", ex.getMessage());
if (ex.getMessage().contains("Duplicate entry")){
String[] msg = ex.getMessage().split(" ");
log.info(Arrays.toString(msg));
return Result.error(msg[2] + MessageConstant.ALREADY_EXISTS);
}else return Result.error(MessageConstant.UNKNOWN_ERROR);
}
}
foreach动态sql
delete from dish where id in(1,2,3...)
<delete id="deleteByIds">
delete from dish where id in
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</delete>
set动态sql
- ,记得别忘了
<update id="update">
update dish
<set>
<if test="name!=null">name = #{name},</if>
<if test="categoryId!=null">category_id = #{categoryId},</if>
<if test="price!=null">price = #{price},</if>
<if test="image!=null">image = #{image},</if>
<if test="description!=null">description = #{description},</if>
<if test="status!=null">status = #{status},</if>
<if test="updateTime!=null">update_time = #{updateTime},</if>
<if test="updateUser!=null">update_user = #{updateUser},</if>
</set>
where id = #{id}
</update>
修改小技巧
-
修改如果有不好改的,例如原来有想改成无delete,原来无想改成有insert
-
可以先删除原来,再插入新数据实现修改
public void update(DishDTO dishDTO) { Dish dish = new Dish(); BeanUtils.copyProperties(dishDTO,dish); // 修改菜品 dishMapper.update(dish); // 修改口味 = 先删除全部,再新增口味 dishFlavorMapper.deleteByDishId(dishDTO.getId()); List<DishFlavor> flavors = dishDTO.getFlavors(); if (flavors!=null && flavors.size()>0){ flavors.forEach(dishFlavor -> { dishFlavor.setDishId(dishDTO.getId()); }); dishFlavorMapper.insertBatch(flavors); } }
动态sql和普通sql易错点
-
插入修改数据过多,分清楚普通sql语句是数据库字段,下划线,update_time
#{}引用的是实体类,驼峰,#{updateTime}
不要直接复制,记得看一眼
<insert id="insert" useGeneratedKeys="true" keyProperty="id"> insert into setmeal (category_id, name, price, status, description, image, create_time, update_time, create_user, update_user) VALUES (#{categoryId},#{name},#{price},#{status},#{description},#{image},#{createTime},#{updateTime},#{createUser},#{updateUser}) </insert>
-
修改update对应动态set
记得
包裹,并且每一个 记得最后加逗号, <update id="update"> update setmeal <set> <if test="categoryId!=null">category_id = #{categoryId},</if> <if test="name!=null">name = #{name},</if> <if test="price!=null">price = #{price},</if> <if test="status!=null">status = #{status},</if> <if test="description!=null">description = #{description},</if> <if test="image!=null">image = #{image},</if> <if test="updateTime!=null">update_time = #{updateTime},</if> <if test="updateUser!=null">update_user = #{updateUser},</if> </set> where id = #{id} </update>
-
多条件查询动态where
标签包裹,每一个 前面记得加and <select id="pageQuery" resultType="com.sky.vo.SetmealVO"> select s.*,c.name as categoryName from setmeal s left join category c on s.category_id = c.id <where> <if test="name!=null">and s.name like concat('%',#{name},'%')</if> <if test="status!=null">and s.status = #{status}</if> <if test="categoryId!=null">and s.category_id = #{categoryId}</if> </where> order by s.update_time desc </select>
-
批量插入多条数据
foreach 用法
insert into dish () values (),(),()
<insert id="insertBatch"> insert into setmeal_dish (setmeal_id, dish_id, name, price, copies) VALUES <foreach collection="setmealDishes" item="sd" separator=","> (#{sd.setmealId},#{sd.dishId},#{sd.name},#{sd.price},#{sd.copies}) </foreach> </insert>
-
查询多条数据
foreach 用法
select * from dish where id in(1,2,3...)
<select id="getSetmealIdsbyDishIds" resultType="java.lang.Long"> select setmeal_id from setmeal_dish where dish_id in <foreach collection="ids" item="id" separator="," open="(" close=")"> #{id} </foreach> </select>
-
模糊查询
concat
name like concat('%',#{name},'%')
mysql语法crud
redis
- 非关系型数据库,存储方式k-v键值对
- 存数据在内存之中,热点读写更快
- 在项目中和mysql互补,mysql存大多数数据
cmd启动redis
- 文件夹cmd
- ctrl + c 关闭服务
- 开启服务后,客户端可以连接,有密码要加密码,exit退出
cmd小操作
- 关闭服务 ctrl+c
- 退出exit
- 再调用上一次输入命令 移动键上和右
- 清屏cls
- tab 根据文件夹内文件名自动补全
redis数据类型
- 数据类型指的是value,key只有字符串这一种
字符串string操作
-
set k v
-
get k
-
setex k 时间(秒)v
-
setnx k v
不存在这个k才执行
哈希表map操作
-
map集合,key对应多个field-value,一般存对象,field value都是string
-
hset k f v
-
hget k f
-
hdel k f (删光f,系统会自动删除这个k)
-
hkeys k 获取所有field
-
hvals k 获取所有value
列表list操作
- 跟list集合,链表一样,有序有下标可重复,双向链表
- lpush k v 从k的头部插入v
- lrange k start stop 返回k的[sta,stop],stop=-1表示到最后
- rpop k 删除最后一个v 返回这个v
- llen k 返回k的长度
集合set操作
- 跟set集合一样,无序无下标不可重复
- sadd k v...
- smembers k 返回全部v
- scard k 返回v的数量
- sinter k1 k2 返回k1,2交集
- sunion k1 k2 返回 k1,k2并集
- srem k1 v.. 删除这个v
有序集合zset操作
-
集合set加一个double分数用于排序,本质还是set,元素不能重复
-
zadd k score v,score代表分数
-
zrange k 0 -1 withscores,从分数小到大查询[0,-1]元素加上分数
-
zincrby k score v ,给v的分数加5
-
zrem k v,移除v
key操作
- 直接操作key
- keys * 返回所有的k keys set* 返回所有开头是set的k
- exists k 当前k是否存在,存在就1,不存在0
- type k 当前k类型
- del k1 k2 删除k1k2
java操作redis
-
导入坐标
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
-
编写连接配置
redis: host: localhost port: 6379 password: 123456 database: 10
-
创建redis配置类,用于创建redistemplate对象,交给spring
/** * redis配置类 * 创建RedisTemplate对象 */ @Configuration @Slf4j public class RedisConfiguration { // RedisConnectionFactory由坐标初始化类用配置的数据创建,由spring管理 @Bean public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){ log.info("redis连接工厂初始化:{}",redisConnectionFactory); RedisTemplate redisTemplate = new RedisTemplate(); // 设置redis 的连接工厂对象 redisTemplate.setConnectionFactory(redisConnectionFactory); // 设置redis key的序列化器 redisTemplate.setKeySerializer(new StringRedisSerializer()); return redisTemplate; } }
-
测试
@SpringBootTest public class SpringDataRedisTest { @Autowired private RedisTemplate redisTemplate; @Test public void testRedisTemplate(){ System.out.println(redisTemplate); // 从上到下 string hash list set zset ValueOperations valueOperations = redisTemplate.opsForValue(); HashOperations hashOperations = redisTemplate.opsForHash(); ListOperations listOperations = redisTemplate.opsForList(); SetOperations setOperations = redisTemplate.opsForSet(); ZSetOperations zSetOperations = redisTemplate.opsForZSet(); } }
string类型
@Test
public void testValueOperations(){
ValueOperations valueOperations = redisTemplate.opsForValue();
valueOperations.set("name","小明");
System.out.println(valueOperations.get("name"));
System.out.println(valueOperations.get("name").toString());
valueOperations.set("code","1234",3, TimeUnit.MINUTES);
valueOperations.setIfAbsent("city","北京");
valueOperations.setIfAbsent("city","上海");
}
hash类型
@Test
public void testHashOperations(){
HashOperations hashOperations = redisTemplate.opsForHash();
// hset hget hkeys hvals hdel
hashOperations.put("100","name","tom");
hashOperations.put("100","age","18");
String name = (String) hashOperations.get("100", "name");
System.out.println(name);
Set keys = hashOperations.keys("100");
System.out.println(keys);
List values = hashOperations.values("100");
System.out.println(values);
hashOperations.delete("100","age");
}
list类型
@Test
public void testListOperations(){
ListOperations listOperations = redisTemplate.opsForList();
// lpush llen rpop lrange
listOperations.leftPushAll("list1","a","b","c");
listOperations.leftPush("list1","d");
List list1 = listOperations.range("list1", 0, -1);
System.out.println(list1);
listOperations.rightPop("list1");
Long list11 = listOperations.size("list1");
System.out.println(list11);
}
set类型
@Test
public void testSetOperations() {
SetOperations setOperations = redisTemplate.opsForSet();
// sadd smembers sinter sunion srem scard
setOperations.add("set1","a","b","c","d");
setOperations.add("set2","a","b","x","y");
Long set1 = setOperations.size("set1");
System.out.println(set1);
Set set11 = setOperations.members("set1");
System.out.println(set11);
Set intersect = setOperations.intersect("set1", "set2");
System.out.println(intersect);
Set union = setOperations.union("set1", "set2");
System.out.println(union);
setOperations.remove("set1","a","b");
}
zset类型
@Test
public void testZSetOperations(){
ZSetOperations zSetOperations = redisTemplate.opsForZSet();
// zadd zrange zrem zincrby
zSetOperations.add("zset","a",10);
zSetOperations.add("zset","b",13);
zSetOperations.add("zset","c",15);
Set zset = zSetOperations.range("zset", 0, -1);
System.out.println(zset);
zSetOperations.incrementScore("zset","a",6);
zSetOperations.remove("zset","a","b");
}
通用key操作
@Test
public void testKeyCommon(){
// keys exists type del
Set keys = redisTemplate.keys("*");
System.out.println(keys);
Boolean name = redisTemplate.hasKey("name");
Boolean name1 = redisTemplate.hasKey("name1");
for (Object key : keys) {
System.out.println(redisTemplate.type(key));
}
redisTemplate.delete("set1");
}
序列化和反序列化
-
序列化和反序列化就像生活中的打包和拆包过程:
-
序列化(打包)
假设你有一套乐高玩具,想寄给朋友。你需要把零件拆开,按顺序摆进盒子里(比如说明书放最上面),这就是序列化——把复杂的数据(比如对象、游戏存档)变成一串可以存储或传输的格式(如JSON、二进制)。
-
反序列化(拆包)
朋友收到盒子后,按照说明书把零件重新拼成乐高模型,这就是反序列化——把存储的格式(如文件、网络传输的数据)还原成原来的数据。
举个栗子🌰:
- 保存游戏进度:把角色位置、装备等序列化成存档文件
- 加载游戏:读取存档文件并反序列化回内存中的游戏数据
- 微信发消息:聊天内容先序列化成数据包,对方收到后再反序列化成文字
- 简单说就是: 序列化 = 把东西变成能保存/传输的格式 反序列化 = 把保存的格式还原成原来的东西
-
枚举类enum
-
Java 枚举类(Enum)是一种特殊的类,用于表示一组固定的常量。它通过
enum
关键字定义,提供了一种更安全、更直观的方式来表示有限的、预定义的值集合(如星期、状态、颜色等)。枚举类的核心特点
- 固定常量集合 枚举类的值在定义时确定,不可在运行时修改。
- 类型安全 编译时检查,避免使用无效值(如
if(status == 1)
中的魔法数字)。 - 可添加方法和字段 枚举类可以有构造方法、普通方法、字段,甚至实现接口。
- 单例特性 每个枚举常量都是枚举类的唯一实例,天然线程安全。
- 内置方法 如
values()
,valueOf()
,name()
,ordinal()
等。
不同包的相同名类给spring bean托管会报错
-
因为bean托管不能重名,默认restcontroller不指定就是自动以小写命名bean
-
解决方法
restcontroller("x")
restcontroller("y")
自定义指定不同的bean名字就行
swageer多个包分类
-
复制两份docket,分别扫两个包,可以实现分开查看
@Bean public Docket docketAdmin() { log.info("swagger文档"); 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 docketUser() { log.info("swagger文档"); 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; }
httpservlet和httpclient
- HttpServlet:服务端处理 HTTP 请求的核心组件。
- HttpClient:客户端发送 HTTP 请求的工具库。
- 联系:两者基于 HTTP 协议协作,但方向相反,共同支持完整的 HTTP 通信场景。
特性 | HttpServlet | HttpClient |
---|---|---|
角色 | 服务端组件(处理请求) | 客户端组件(发送请求) |
所属技术栈 | Java Servlet API(如 Tomcat、Jetty) | 第三方库(如 Apache HttpClient、OkHttp) |
主要用途 | 接收并响应 HTTP 请求(如处理浏览器请求) | 主动向其他服务发送 HTTP 请求 |
典型场景 | 实现 Web 应用的接口(如处理表单提交) | 调用外部 API(如访问 RESTful 服务) |
httpclient测试
- 导入maven
@SpringBootTest
public class HttpClientTest {
@Test
public void testHttpGet() throws IOException {
// 创建httpClient对象
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);
// 获得返回数据,解析entity
HttpEntity entity = response.getEntity();
String s = EntityUtils.toString(entity);
System.out.println("返回数据为:"+s);
// 关闭资源
response.close();
httpClient.close();
}
@Test
public void testHttpPost() throws IOException {
// 创建httpClient
CloseableHttpClient httpClient = HttpClients.createDefault();
// 创建post请求
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.setContentEncoding("utf-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
// 处理请求返回数据
CloseableHttpResponse response = httpClient.execute(httpPost);
int statusCode = response.getStatusLine().getStatusCode();
System.out.println("执行状态码:"+statusCode);
HttpEntity entity1 = response.getEntity();
String s = EntityUtils.toString(entity1);
System.out.println("执行返回结果:"+s);
// 关闭资源
response.close();
httpClient.close();
}
}
httpclient工具类
-
get 由于地址栏传参,需要封装?k=v,所有url封装成uri
-
post 通过UrlEncodedFormEntity 自带
doPost
方法通过UrlEncodedFormEntity
将参数编码为application/x-www-form-urlencoded
格式的请求体。doPost4Json
方法直接将参数转为JSON格式的请求体。
/**
* Http工具类
*/
public class HttpClientUtil {
static final int TIMEOUT_MSEC = 5 * 1000;
/**
* 发送GET方式请求
* @param url
* @param paramMap
* @return
*/
public static String doGet(String url,Map<String,String> paramMap){
// 创建Httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
String result = "";
CloseableHttpResponse response = null;
try{
URIBuilder builder = new URIBuilder(url);
if(paramMap != null){
for (String key : paramMap.keySet()) {
builder.addParameter(key,paramMap.get(key));
}
}
URI uri = builder.build();
//创建GET请求
HttpGet httpGet = new HttpGet(uri);
//发送请求
response = httpClient.execute(httpGet);
//判断响应状态
if(response.getStatusLine().getStatusCode() == 200){
result = EntityUtils.toString(response.getEntity(),"UTF-8");
}
}catch (Exception e){
e.printStackTrace();
}finally {
try {
response.close();
httpClient.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return result;
}
/**
* 发送POST方式请求
* @param url
* @param paramMap
* @return
* @throws IOException
*/
public static String doPost(String url, Map<String, String> paramMap) throws IOException {
// 创建Httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = null;
String resultString = "";
try {
// 创建Http Post请求
HttpPost httpPost = new HttpPost(url);
// 创建参数列表
if (paramMap != null) {
List<NameValuePair> paramList = new ArrayList();
for (Map.Entry<String, String> param : paramMap.entrySet()) {
paramList.add(new BasicNameValuePair(param.getKey(), param.getValue()));
}
// 模拟表单
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(paramList);
httpPost.setEntity(entity);
}
httpPost.setConfig(builderRequestConfig());
// 执行http请求
response = httpClient.execute(httpPost);
resultString = EntityUtils.toString(response.getEntity(), "UTF-8");
} catch (Exception e) {
throw e;
} finally {
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return resultString;
}
/**
* 发送POST方式请求
* @param url
* @param paramMap
* @return
* @throws IOException
*/
public static String doPost4Json(String url, Map<String, String> paramMap) throws IOException {
// 创建Httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = null;
String resultString = "";
try {
// 创建Http Post请求
HttpPost httpPost = new HttpPost(url);
if (paramMap != null) {
//构造json格式数据
JSONObject jsonObject = new JSONObject();
for (Map.Entry<String, String> param : paramMap.entrySet()) {
jsonObject.put(param.getKey(),param.getValue());
}
StringEntity entity = new StringEntity(jsonObject.toString(),"utf-8");
//设置请求编码
entity.setContentEncoding("utf-8");
//设置数据类型
entity.setContentType("application/json");
httpPost.setEntity(entity);
}
httpPost.setConfig(builderRequestConfig());
// 执行http请求
response = httpClient.execute(httpPost);
resultString = EntityUtils.toString(response.getEntity(), "UTF-8");
} catch (Exception e) {
throw e;
} finally {
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return resultString;
}
private static RequestConfig builderRequestConfig() {
return RequestConfig.custom()
.setConnectTimeout(TIMEOUT_MSEC)
.setConnectionRequestTimeout(TIMEOUT_MSEC)
.setSocketTimeout(TIMEOUT_MSEC).build();
}
}
总结
请求方法 | 参数位置 | 是否需要URI对象 | 原因 |
---|---|---|---|
GET | URL查询字符串 | 是 | 需要构建完整的URL并处理编码 |
POST | 请求体(Body) | 否 | 参数通过Entity设置,URL无需修改 |
这种设计既符合HTTP协议规范,也确保了代码的安全性和可维护性
@Bean @Component @Autowired
- 是的,无论是通过
@Component
(及其派生注解如@Service
,@Controller
等)还是@Bean
注册的 Bean,都可以通过@Autowired
注入。它们的核心区别在于 Bean 的注册方式和适用场景。以下是详细对比:
特性 | @Component |
@Bean |
---|---|---|
注册方式 | 类级别注解,自动扫描注册 | 方法级别注解,手动在配置类中定义 |
适用场景 | 自己编写的类(如 Service、Controller 等) | 需要复杂初始化的类、第三方库的类(如数据库连接池) |
控制权 | Spring 自动实例化 | 开发者手动控制实例化逻辑 |
依赖注入方式 | 自动注入(通过构造函数或字段) | 通过方法参数显式注入依赖 |
Bean 名称 | 默认类名首字母小写(可自定义) | 默认方法名(可指定 name 属性) |
1. 使用 @Component
注册 Bean
假设你有一个自己编写的服务类:
Java@Service // @Component 的派生注解
public class UserService {
public void saveUser() {
// 业务逻辑
}
}
使用方式:Spring 会自动扫描 UserService
并注册为 Bean,其他类可以直接注入:
Java@RestController
public class UserController {
@Autowired // 注入 UserService
private UserService userService;
}
2. 使用 @Bean
注册 Bean
假设你需要集成第三方库的类(例如数据库连接池),或者需要复杂初始化逻辑:
Java@Configuration
public class AppConfig {
@Bean // 手动定义 Bean
public DataSource dataSource() {
// 创建并配置连接池
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
dataSource.setUsername("root");
dataSource.setPassword("123456");
return dataSource;
}
}
使用方式:其他类可以直接注入 DataSource
:
Java@Service
public class OrderService {
@Autowired
private DataSource dataSource; // 注入第三方库的 Bean
}
常量修饰
public static final String url = ""
FastJSON相关
String json = HttpClientUtil.doGet(url, map);
// 解析json字符串成json对象
JSONObject jsonObject = JSON.parseObject(json);
String openid = jsonObject.getString("openid");
return openid;
定义拦截器
-
要加一个用户端jwt拦截校验器
-
首先,JwtTokenUserInterceptor实现HandlerInterceptor
/** * jwt令牌校验的拦截器 */ @Component @Slf4j public class JwtTokenUserInterceptor implements HandlerInterceptor { @Autowired private JwtProperties jwtProperties; /** * 校验jwt在所有controller请求前 * * @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.getUserTokenName()); //2、校验令牌 try { log.info("jwt校验:{}", token); Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(), token); Long userId = Long.valueOf(claims.get(JwtClaimsConstant.USER_ID).toString()); log.info("当前用户id:{}", userId); // 在线程空间存入id BaseContext.setCurrentId(userId); //3、通过,放行 return true; } catch (Exception ex) { //4、不通过,响应401状态码 response.setStatus(401); return false; } } }
-
然后,webmvcconfiguration
在webmvc拦截器列表内添加自己定义拦截器,设置对应的拦截路径
/** * 配置类,注册web层相关组件 */ @Configuration @Slf4j public class WebMvcConfiguration extends WebMvcConfigurationSupport { @Autowired private JwtTokenAdminInterceptor jwtTokenAdminInterceptor; @Autowired private JwtTokenUserInterceptor jwtTokenUserInterceptor; /** * 注册自定义拦截器 * * @param registry */ protected void addInterceptors(InterceptorRegistry registry) { log.info("开始注册自定义拦截器..."); registry.addInterceptor(jwtTokenAdminInterceptor) .addPathPatterns("/admin/**") .excludePathPatterns("/admin/employee/login"); registry.addInterceptor(jwtTokenUserInterceptor) .addPathPatterns("/user/**") .excludePathPatterns("/user/user/login") .excludePathPatterns("/user/shop/status"); }
操作哪个数据库就用哪个controller service mapper
- 查询某个分类下的菜品,就操作菜品dish 对应 controller service
缓存加快速度
-
用户多点餐,每次查询都要调用sql查询数据库,会比较慢,使用把查询的dish数据放入redis存储,加快查询速度
-
修改user查询
@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) { Dish dish = new Dish(); dish.setCategoryId(categoryId); dish.setStatus(StatusConstant.ENABLE);//查询起售中的菜品 // 构造redis的key dish_categoryId String key = "dish_" + categoryId; // 查询先看redis有没有这样的key,(string反序列化list) List<DishVO> list = (List<DishVO>) redisTemplate.opsForValue().get(key); // 如果有,直接返回 if (list!=null && list.size()>0){ log.info("redis缓存存在,直接取出:{}",list); return Result.success(list); } // 没有就查数据库,然后注册到redis中,(list序列化成string) list = dishService.listWithFlavor(dish); redisTemplate.opsForValue().set(key,list); return Result.success(list); } }
-
如果管理端改变了菜品数据,需要清理对应缓存,保证数据一致性
增删改都要
/** * 清理redis缓存 */ private void cleanCache(String pattern){ Set keys = redisTemplate.keys(pattern); log.info("清理redis缓存: {}",keys); redisTemplate.delete(keys); }
@PostMapping("/status/{status}") @ApiOperation("修改菜品状态") public Result startOrStop(@PathVariable Integer status,Long id){ log.info("修改菜品状态 {},{}",status,id); dishService.startOrStop(status,id); // 清理缓存 cleanCache("dish_*"); return Result.success(); }
注解获取自增id,赋值给实体类
@Insert("insert into user(name,age) values (#{name},#{age})")
@Options(useGeneratedKeys = true,keyProperty = "id")
void insert(User user);
redis树形存储
-
k如果中间有: redis会把这个当成树形结构,本质k还是a:b:c:d和userCache::1
Spring-cache注解实现缓存
-
导入哪个底层用哪个,类似导入redis坐标就是redis
-
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
-
导入Springcache坐标
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency>
-
常用方法
@EnableCaching
加启动类上,开启使缓存注解生效
@CachePut
执行方法后,把方法执行结果存入redis
@Cacheable
执行方法前,看redis有没有这个kv,有就直接返回,不执行方法,没有就执行方法,存入redis
@CacheEvict
执行方法后,删除这个kv
@RestController @RequestMapping("/user") @Slf4j public class UserController { @Autowired private UserMapper userMapper; @PostMapping // 把返回结果作为v存入redis, // 执行完方法后执行存redis @CachePut(cacheNames = "userCache",key = "#user.id")// 构造key cacheNames::key public User save(@RequestBody User user){ userMapper.insert(user); return user; } @DeleteMapping // 方法执行完后删除redis @CacheEvict(cacheNames = "userCache",key = "#id") public void deleteById(Long id){ userMapper.deleteById(id); } @DeleteMapping("/delAll") // 方法执行完删除所有userCache下的k @CacheEvict(cacheNames = "userCache",allEntries = true) public void deleteAll(){ userMapper.deleteAll(); } @GetMapping // 本质是创建了一个代理 // 执行方法前先查redis有没有key,有直接返回,不走方法了; // 没有就通过反射执行方法,查数据库,然后把key和返回数据v存入redis // 执行方法前执行操作redis @Cacheable(cacheNames = "userCache",key = "#id")// 构造key public User getById(Long id){ User user = userMapper.getById(id); return user; } }
套餐 分类 菜品
- 分类有套餐分类和菜品分类
- 套餐相当于菜品集合,可以理解为也是一种菜品
jwt令牌校验取出操作的用户(员工)id
-
登录注册时,自增id放入对象里
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
-
生成jwt令牌后,放入id,加密成token
// 生成jwt令牌 Map<String, Object> claims = new HashMap<>(); claims.put(JwtClaimsConstant.USER_ID,user.getId()); String token = JwtUtil.createJWT(jwtProperties.getUserSecretKey(), jwtProperties.getUserTtl(), claims);
-
jwt拦截器从请求头获取令牌,解密出userid,放入线程空间
BaseContext.setCurrentId(userId);
//1、从请求头中获取令牌 String token = request.getHeader(jwtProperties.getUserTokenName()); //2、校验令牌 try { log.info("jwt校验:{}", token); Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(), token); Long userId = Long.valueOf(claims.get(JwtClaimsConstant.USER_ID).toString()); log.info("当前用户id:{}", userId); // 在线程空间存入id BaseContext.setCurrentId(userId); //3、通过,放行 return true; } catch (Exception ex) { //4、不通过,响应401状态码 response.setStatus(401); return false; }
-
所以每次操作会携带令牌,验证令牌后,线程空间就有操作者的id,可以取出
BaseContext.getCurrentId()
@Override public List<ShoppingCart> list() { ShoppingCart shoppingCart = ShoppingCart .builder() .userId(BaseContext.getCurrentId()) .build(); return shoppingCartMapper.list(shoppingCart); }
冗余字段设置
-
方便业务操作,避免需要多次查询数据库,为了后续业务更简单
-
这里地址id可以查到地址信息和手机号,但为了方便展示,直接设计成冗余字段
BeanUtils.copyProperties使用
-
这里的
BeanUtils.copyProperties(addressBook,orders);
和
BeanUtils.copyProperties(cart,orderDetail);
虽然会复制id,但可以正常使用,因为插入不插入id,id为数据库自增
修改会传入id,所以不能直接复制,要set
@Transactional public OrderSubmitVO submit(OrdersSubmitDTO ordersSubmitDTO) { // 处理业务异常 // 校验提交的地址薄是否为空,空就抛异常 AddressBook addressBook = addressBookMapper.getById(ordersSubmitDTO.getAddressBookId()); if (addressBook==null){ throw new AddressBookBusinessException(MessageConstant.ADDRESS_BOOK_IS_NULL); } // 购物车是否为空 Long userId = BaseContext.getCurrentId(); ShoppingCart shoppingCart = ShoppingCart .builder() .userId(userId) .build(); List<ShoppingCart> shoppingCartList = shoppingCartMapper.list(shoppingCart); if (shoppingCartList==null || shoppingCartList.size()==0){ throw new ShoppingCartBusinessException(MessageConstant.SHOPPING_CART_IS_NULL); } // 都不空插入一条订单数据 Orders orders = new Orders(); BeanUtils.copyProperties(ordersSubmitDTO,orders); orders.setNumber(String.valueOf(System.currentTimeMillis()));// 以时间戳为订单号 orders.setStatus(Orders.PENDING_PAYMENT); orders.setUserId(userId); orders.setOrderTime(LocalDateTime.now()); orders.setPayStatus(Orders.UN_PAID); // 这里会复制id,但也不影响,因为插入是自增 BeanUtils.copyProperties(addressBook,orders); // orders.setUserName(addressBook.getConsignee()); // orders.setPhone(addressBook.getPhone()); // orders.setAddress(addressBook.getDetail()); // orders.setConsignee(addressBook.getConsignee()); orderMapper.insert(orders); // 插入多条订单详情数据,订单详情就是订单对应的菜品套餐详情 List<OrderDetail> orderDetails = new ArrayList<>(); for (ShoppingCart cart : shoppingCartList) { OrderDetail orderDetail = OrderDetail .builder() .orderId(orders.getId()) .build(); // 这里会复制id,但没有影响,因为插入不插入id,id自增不影响 BeanUtils.copyProperties(cart,orderDetail); orderDetails.add(orderDetail); } orderDetailMapper.insertBatch(orderDetails); // 清空购物车 shoppingCartMapper.clean(userId); // 构造返回类型 OrderSubmitVO orderSubmitVO = OrderSubmitVO .builder() .id(orders.getId()) .orderNumber(orders.getNumber()) .orderAmount(orders.getAmount()) .orderTime(orders.getOrderTime()) .build(); return orderSubmitVO; }
微信支付
一对多放入page
-
controller
/** * 分页查询历史订单 * @param ordersPageQueryDTO * @return */ @GetMapping("/historyOrders") @ApiOperation("分页查询历史订单") public Result<PageResult> page(OrdersPageQueryDTO ordersPageQueryDTO){ log.info("分页查询历史订单: {}",ordersPageQueryDTO); PageResult pageResult = orderService.page(ordersPageQueryDTO); return Result.success(pageResult); }
-
service
这里是一对多
如果连表查,数据会有问题,所以要先查订单,再通过订单查详情,最后放一起返回
/** * 分页查询历史订单 * @param ordersPageQueryDTO * @return */ @Override public PageResult page(OrdersPageQueryDTO ordersPageQueryDTO) { ordersPageQueryDTO.setUserId(BaseContext.getCurrentId()); PageHelper.startPage(ordersPageQueryDTO.getPage(),ordersPageQueryDTO.getPageSize()); // 分页查询所有该用户不同状态的订单 Page<Orders> page = orderMapper.page(ordersPageQueryDTO); // 遍历所有订单取出订单详情 List<OrderVO> voList = new ArrayList<>(); if (page!=null && page.size()>0){ for (Orders order : page) { OrderVO orderVO = new OrderVO(); List<OrderDetail> orderDetails = orderDetailMapper.getByOrderId(order.getId()); BeanUtils.copyProperties(order,orderVO); orderVO.setOrderDetailList(orderDetails); // 放入OrdersVO集合,返回集合结果(一对多) voList.add(orderVO); } } return new PageResult(page.getTotal(),voList); }
如果是多对一,类似查菜品和对应分类,或者一对一
可以直接连表
Page<DishVO> page = dishMapper.pageQuery(dishPageQueryDTO);
-
mapper
<select id="page" resultType="com.sky.entity.Orders"> select * from orders <where> <if test="status!=null">and status=#{status}</if> <if test="userId!=null">and user_id=#{userId}</if> </where> </select>
Mybatis大于小于
<if test="beginTime != null">
and order_time >= #{beginTime}
</if>
<if test="endTime != null">
and order_time <= #{endTime}
</if>
调用API流程
-
配置令牌,和发请求需要的参数
看官方文档,获取请求方式和url以及需要的参数和返回
-
请求工具类,发送请求获取结果
-
解析结果
/** * 检查客户的收货地址是否超出配送范围 * @param address */ private void checkOutOfRange(String address){ Map<String,String> map = new HashMap<>(); map.put("ak",ak); map.put("address",shopAddress); map.put("output","json"); // 获取店铺经纬坐标 String shopCoordinate = HttpClientUtil.doGet("https://api.map.baidu.com/geocoding/v3", map); JSONObject jsonObject = JSONObject.parseObject(shopCoordinate); if (!jsonObject.getString("status").equals("0")){ throw new OrderBusinessException("地址解析失败"); } JSONObject location = jsonObject.getJSONObject("result").getJSONObject("location"); String lng = location.getString("lng"); String lat = location.getString("lat"); String shopLatLng = lat + "," +lng; // 获取订单用户的经纬坐标 map.put("address",address); String userCoordinate = HttpClientUtil.doGet("https://api.map.baidu.com/geocoding/v3", map); jsonObject = JSONObject.parseObject(userCoordinate); if (!jsonObject.getString("status").equals("0")){ throw new OrderBusinessException("地址解析失败"); } location = jsonObject.getJSONObject("result").getJSONObject("location"); lng = location.getString("lng"); lat = location.getString("lat"); String userLatLng = lat + "," +lng; // 算路线 map.put("origin",shopLatLng); map.put("destination",userLatLng); map.put("steps_info","0"); String json = HttpClientUtil.doGet("https://api.map.baidu.com/directionlite/v1/driving", map); jsonObject = JSONObject.parseObject(json); if (!jsonObject.getString("status").equals("0")){ throw new OrderBusinessException("路线解析失败"); } JSONObject result = jsonObject.getJSONObject("result"); JSONArray routes = result.getJSONArray("routes"); Integer distance = Integer.valueOf(((JSONObject)routes.get(0)).getString("distance")); if (distance>5000){ throw new OrderBusinessException("超出配送范围"); } }
Cron表达式
-
分6(7)块域,秒分时日月周(年可选)
-
周和日互斥,一般有一个,另一个是?
-
直接搜在线cron生成器
0 1-2 0/1 1W 7 L *
SpirngTask
-
定时任务
-
类似闹钟
-
使用流程
-
导入坐标
在spring框架自带,context里面
-
主启动类注解开启开关
@EnableScheduling // 开启任务调度
-
编写Task类,加@Component注解
方法加@scheduled注解,配置定时cron表达式,编写需要按条件定时执行的业务代码
/** * 定时处理订单 */ @Component @Slf4j public class OrderTask { @Autowired private OrderMapper orderMapper; /** * 处理超时订单 * 每分钟检查一次 */ @Scheduled(cron = "0 * * * * ?") public void processTimeoutOrder(){ log.info("定时检查超时订单,当前时间:{},", LocalDateTime.now()); // 找到超时15min且状态为待支付的订单 // select * from orders where status = ? and create_time < (当前时间-15min) Integer status = Orders.PENDING_PAYMENT; LocalDateTime orderTime = LocalDateTime.now().plusMinutes(-15); List<Orders> ordersList = orderMapper.getByStatusAndTimeLT(status,orderTime); // 修改订单状态 if (ordersList!=null && ordersList.size()>0){ for (Orders orders : ordersList) { orders.setStatus(Orders.CANCELLED); orders.setCancelTime(LocalDateTime.now()); orders.setCancelReason("用户支付超时,自动取消订单"); orderMapper.update(orders); } } } /** * 处理派送中订单 * 每天凌晨1点处理 */ @Scheduled(cron = "0 0 1 * * ?") public void processDeliverOrders(){ log.info("定时处理昨日订单,执行时间:{}",LocalDateTime.now()); // 查询状态为派送中,且下单时间是当前时间每日凌晨1点前(-60) Integer status = Orders.DELIVERY_IN_PROGRESS; LocalDateTime orderTime = LocalDateTime.now().plusMinutes(-60); List<Orders> ordersList = orderMapper.getByStatusAndTimeLT(status,orderTime); // 修改订单状态 if (ordersList!=null && ordersList.size()>0){ for (Orders orders : ordersList) { orders.setStatus(Orders.COMPLETED); orderMapper.update(orders); } } } }
-
WebSocket
- 基于tcp新网络协议
- websocket和http都是基于tcp,先连接再发
- http 短连接,基于请求响应,单向
- websocke 长连接,握手协议后就是双向数据传输,服务器可以给客户端数据传输,客户端也可以给服务器数据传输
websocket使用
-
导入websocket坐标
-
编写前端html浏览器,客户端
回调函数,配置路径
-
编写后端websocket配置
/** * 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(); } } } }
-
编写配置类开启@ServerEndpoint注解
package com.sky.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.server.standard.ServerEndpointExporter; /** * WebSocket配置类,用于注册WebSocket的Bean */ @Configuration public class WebSocketConfiguration { /** * 配置类注册ServerEndpointExporter * ServerEndpointExporter会扫描所有的websocket使他们生效 * @return */ @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }
-
测试
@Component public class WebSocketTask { @Autowired private WebSocketServer webSocketServer; /** * 通过WebSocket每隔5秒向客户端发送消息 */ @Scheduled(cron = "0/5 * * * * ?") public void sendMessageToClient() { webSocketServer.sendToAllClient("这是来自服务端的消息:" + DateTimeFormatter.ofPattern("HH:mm:ss").format(LocalDateTime.now())); } }
配置类@Configuration
-
加完@Configuration相当于spring配置文件
注册bean时使用
这种就是把ServerEndpointExporter交给spring管理,项目启动就被注册到spring托管
<id="serverEndpointExporter" class ="ServerEndpointExporter">
package com.sky.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.server.standard.ServerEndpointExporter; /** * WebSocket配置类,用于注册WebSocket的Bean */ @Configuration public class WebSocketConfiguration { /** * 配置类注册ServerEndpointExporter * ServerEndpointExporter会扫描所有的websocket使他们生效 * @return */ @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }
-
@component也是注册bean,需要配置类扫包,但主启动类自动扫子包
-
autowired就是找getbyname 找id一样的类bean,自动注入spring容器里面的bean
apache echarts
-
数据可视化计算
-
柱状图,饼状图,折线图
-
前端要求规定数据格式
LocalDateTime和LocalDate转化
-
LocalDate年月日,LocalDateTime年月日时分秒
LocalDateTime beginTime = LocalDateTime.of(localDate, LocalTime.MIN); LocalDateTime endTime = LocalDateTime.of(localDate, LocalTime.MAX);
StringUtils(lang3)
-
把列表转化为字符串的工具,还可以有分割符
-
join方法
String turnoverList = StringUtils.join(sumList,",");
2022-10-01,2022-10-02,2022-10-03
时间格式
- 定义前端传入格式
public Result<TurnoverReportVO> turnoverReport(
@DateTimeFormat(pattern = "yyyy-MM-dd")
LocalDate begin,
@DateTimeFormat(pattern = "yyyy-MM-dd")
LocalDate end){
TurnoverReportVO turnoverReportVO = reportService.getTurnoverReport(begin,end);
return Result.success(turnoverReportVO);
}
==和equals
- ==基本类型比值,引用比地址
- equals比值
- 一般equals用的多
websocket引起@springbootTest的bug
-
原因分步解释
ServerEndpointExporter
的作用 WebSocket 配置类(如WebSocketConfiguration
)通常包含一个ServerEndpointExporter
Bean,它的作用是:- 扫描所有
@ServerEndpoint
注解的类。 - 将这些 WebSocket 端点注册到 Servlet 容器的
ServerContainer
中。 - 关键依赖:
ServerContainer
是 Servlet 容器(如 Tomcat)提供的接口,只有在真实 Servlet 容器中才存在。
- 扫描所有
- 测试环境的默认行为 当使用
@SpringBootTest
时:- 默认模式:
webEnvironment = WebEnvironment.MOCK
(模拟 Servlet 环境)。 - 问题:此模式不会启动真实的 Servlet 容器,仅模拟 HTTP 请求(如
MockMvc
)。 - 结果:
ServerContainer
不可用,导致ServerEndpointExporter
初始化失败。
- 默认模式:
- 上下文加载的完整性 Spring 测试要求完整加载应用上下文:
- 如果某个 Bean 初始化失败(如
ServerEndpointExporter
),整个上下文将无法加载。 - 错误表现:
Failed to load ApplicationContext
+ServerContainer not available
。
- 如果某个 Bean 初始化失败(如
-
WebSocket 配置类
package com.sky.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.server.standard.ServerEndpointExporter; /** * WebSocket配置类,用于注册WebSocket的Bean */ @Configuration public class WebSocketConfiguration { /** * 配置类注册ServerEndpointExporter * ServerEndpointExporter会扫描所有的websocket使他们生效 * @return */ @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }
-
WebSocket 配置类导致测试失败的核心矛盾在于:
- 依赖冲突:
ServerEndpointExporter
需要 Servlet 容器的ServerContainer
。 - 环境差异:测试默认不启动真实 Servlet 容器,无法满足依赖。
-
通过调整测试的 Web 环境或条件化配置,即可解决这一问题。
-
解决方法
方法 1:启动真实 Servlet 容器(推荐)
在测试类中显式指定启动嵌入式容器(如 Tomcat):
Java@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class MyTest { // 测试代码 }
方法 2:条件化配置 WebSocket
在 WebSocket 配置类中添加条件注解,避免在非 Servlet 环境中初始化:
Java@Configuration @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) // 仅限 Servlet 环境 public class WebSocketConfiguration { @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }
stream流
- 遍历结果放入集合
List<String> nameList = salesDTOList.stream().map(GoodsSalesDTO::getName).collect(Collectors.toList());
List<Integer> numberList = salesDTOList.stream().map(GoodsSalesDTO::getNumber).collect(Collectors.toList());
这两行代码使用Java 8的Stream API对
salesDTOList
(一个包含GoodsSalesDTO
对象的列表)进行转换,分别提取出每个对象的name
和number
属性,并收集成两个独立的列表。具体解释如下:第一行代码
JavaList<String> nameList = salesDTOList.stream() .map(GoodsSalesDTO::getName) .collect(Collectors.toList());
salesDTOList.stream()
将salesDTOList
转换为一个流(Stream),以便使用流式操作(如map
、filter
等)。.map(GoodsSalesDTO::getName)
对每个GoodsSalesDTO
对象调用getName()
方法,将流中的每个元素(DTO对象)转换为它的name
属性(String类型)。
GoodsSalesDTO::getName
是方法引用,等价于:dto -> dto.getName()
。.collect(Collectors.toList())
将转换后的流(包含所有name
字符串)收集到一个新的List<String>
中,赋值给nameList
。结果:
nameList
是一个包含所有GoodsSalesDTO
对象名称的字符串列表。第二行代码
JavaList<Integer> numberList = salesDTOList.stream() .map(GoodsSalesDTO::getNumber) .collect(Collectors.toList());
salesDTOList.stream()
同样将salesDTOList
转换为流。.map(GoodsSalesDTO::getNumber)
对每个GoodsSalesDTO
对象调用getNumber()
方法,将流中的每个元素(DTO对象)转换为它的number
属性(Integer类型)。.collect(Collectors.toList())
将转换后的流(包含所有number
整数值)收集到一个新的List<Integer>
中,赋值给numberList
。结果:
numberList
是一个包含所有GoodsSalesDTO
对象数量的整数列表。总结
目的:将对象列表中的两个不同属性(
name
和number
)分别提取成两个独立的列表。输入:
List<GoodsSalesDTO> salesDTOList
输出
:
nameList
:List<String>
,包含所有商品的名称。numberList
:List<Integer>
,包含所有商品的数量。类比
假设
GoodsSalesDTO
表示商品销售记录,包含商品名称和销售数量。 原始数据:[{name: "Apple", number: 10}, {name: "Banana", number: 20}]
运行代码后:
nameList
→["Apple", "Banana"]
numberList
→[10, 20]
DTO封装查询对象
-
查询结果多个用list接收
-
先看sql查询结果是如何
<select id="getTop10" resultType="com.sky.dto.GoodsSalesDTO"> select od.name,sum(od.number) as 'number' from order_detail od,orders o where o.id = od.order_id and o.status = 5 <if test="beginTime!=null">and o.order_time > #{beginTime}</if> <if test="endTime!=null">and o.order_time < #{endTime}</if> group by od.name order by number desc limit 0,10 </select>
-
这里结果是name 和 number(别名),所有对应DTO对象也应该是这两个
这里name对应name,number对应别名number,不能错
public class GoodsSalesDTO implements Serializable { //商品名称 private String name; //销量 private Integer number; }
-
接收成功,因为查出来可能多个,所有要用集合接收
List<GoodsSalesDTO> salesDTOList = orderMapper.getTop10(beginTime,endTime);
-