外卖项目实战
一、软件开发整体介绍
1. 软件开发流程
- 需求分析(需求规格说明书、产品原型)
- 设计(UI设计、数据库设计、接口设计)
- 编码(项目代码、单元测试)
- 测试(测试用例)
- 运维(软件环境安装、配置)
2.角色分工
3.软件环境
- 开发环境:开发人员在开发阶段使用的环境,一般外部用户无法访问
- 测试环境:专门给测试人员使用的环境,用于测试项目,一般外部用户无法访问
- 生产环境:即线上环境,正式提供对外服务的环境
二、外卖项目介绍
1.项目介绍
2.产品原型
用于展示项目的业务功能,一般由产品经理进行设计
3.技术选型
展示项目中使用到的技术框架和中间件等
4.前后端分离开发流程
三、开发环境搭建
1.前端环境搭建
直接打开执行打包好的前端项目
2.后端环境搭建
1)使用Git进行版本控制(idea版本)
- 创建Git本地仓库
- 创建Git远程仓库
- 将本地文件推送到Git远程仓库
点击VCS→点击Create Git Repository→选中当前项目的根目录(出现Git常用图标则表示成功)→点击图标√→全部勾选提交到本地仓库点击Commit→打开git网站创建一个新的项目复制地址→在idea中点击斜上箭头定义远程仓库输入原本复制好的地址→点击push提交
2)数据库环境搭建
根据数据库设计文档要求通过数据库建表语句创建数据库表结构
四、业务开发思想逻辑总结
1. 涉及一个请求接口往多个数据库表中添加数据的情况
在项目开发中,我们往往会遇见这样的业务场景:前端提交过来的表单数据需要添加到不同的数据库表中去(是为了开发过程中更加便利,也是为了防止一个表单数据里面有多个集合对象,如果这些数据都放在一个数据库表中不好处理)。例如我现在有一个学生表、老师表,学生表里面有学号、姓名、班级等基本字段。但是我们想要在前端表单数据中去给这个学生添加上课老师姓名的集合,这个时候我们就需要将这个集合批量添加到老师表中去,为了对应所以老师表里面还要有对应学生id。
具体实现步骤:
- 创建添加学生信息的接口,然后参数对象DTO包含前端传送过来的数据(包括老师集合)
- 在service实现类进行添加操作的方法前一点要进行事务管理的注解,使得数据添加一致性(要么都成功,要么都失败)
- 然后在service中进行学生对象(只包含学生基本数据,不包括老师集合)信息拷贝,将信息从DTO中对应拷贝,然后进行学生表的信息添加,因为在后面添加老师表的时候还需要用到学生id去标识。所以在添加学生表信息的sql语句中还要设定返回刚生成的学生id并赋值给学生对象,让后面可以通过get方法去获取。
- 在同一个service中,进行老师表的批量添加操作,操作前先通过遍历,给每一个老师对象都赋值上前面刚生成的学生id。再通过sql去进行动态批量添加。
2. 业务逻辑思维梳理
- 添加数据
前端提交的表单页面里面有多个表的内容,我们需要在后端业务处理层对其进行拆分分别在添加进多个表。
首先,对前端数据进行整体的类封装(DTO)传递到对应的接口,在接口处(Controller层)进行业务层的传递调用;然后,在业务处理层进行内容的拆分,可以使用BeanUtils.copyProperties(DTO,新建对象)进行一个内容的传递,然后分别调用mapper进行添加内容。对于集合对象,我们可以使用动态批量插入的办法(在sql语句里面使用foreach)进行添加(其中可能还有为每个对象赋值,这个时候就需要遍历出其中的对象,为每个对象进行赋值操作,一般都是外键id值的赋值,我们需要获取到前一个刚刚添加好的id值,就需要前一个在执行添加操作的时候,sql语句要设置成返回主键值id,并将id赋值给类对象的id属性,这样我们在外面就可以使用对象的get方法去获取到新添加的字段的id值),这样我们就把内容分别添加进了多个表。一定注意,因为涉及多个表的添加,在业务逻辑层的实现类的方法上,我们要添加事务处理的注解@Transactional,以此保证数据的一致性。 - 批量删除
前端存在勾选多个数据,执行全部删除的业务功能。
前端传递过来的数据如果是String类型的id字符串,那么我们可以通过注解@RequestParam让SpringBoot框架帮我们解析为Long类型的集合,然后传递给业务处理层进行批量删除(删除的时候我们还要考虑业务间的逻辑问题,是否存在数据库表关联冲突的问题,如果有则不能删除(自定义一个异常抛出),有其他表的数据操作时一定要添加事务处理的注解@Transactional),然后我们可以在业务逻辑层进行遍历删除也可以通过sql语句进行批量删除(建议使用这种) - 查询多个表数据
一样的操作,多个表查询后封装到一个对象里面返回给前端。 - 修改操作
传递的修改数据是多个表的数据,这个时候就需要将其拆分出来,然后该对应修改的调用对应的mapper,与其关联的表数据不好进行修改的就先删除再添加达到修改的目的
一个数据需要多个表参与的时候,就要考虑到表与表之间的关系,通过拆分或者封装进行多个表的数据区分(多个表同时操作数据库表内容的时候一定要进行事务处理)。遇到批量处理的业务,集合通过sql语句进行批量添加或删除。
五、总结项目开发技术点
1. Maven分模块设计
将项目按照功能拆分成若干个子模块,方便项目的管理维护、扩展,也方便模块间的相互调用,资源共享。分模块开发需要先针对模块功能进行设计,再进行编码,不能将工程开发完毕后再进行拆分。
当我想用一个项目模块中的一个pojo实体类时,我不能将整个项目都引入到我创建的项目里面(这样对另一个项目模块中的数据不安全),这个时候就可以采用分模块设计,单独创建一个模块项目将pojo放在里面,然后其他两个项目模块需要用到pojo实体类时就引入pojo模块对应的依赖,具体步骤操作如下:
- 创建maven模块,存放实体类
- 将pojo模块的依赖写入需要的模块pom中去
但是这样操作的话可能会重复配置多个依赖(多个模块配置同样的依赖),导致繁琐的操作,因此我们又引入继承与聚合的操作。
1)继承
单独创建一个工程为父工程,在里面写入子工程重复配置的依赖,这样子工程就可以继承父工程,就不用自己去写依赖,减少了依赖重写度。
-
创建父工程的时候打包方式要设置为pom
-
用< parent>…< /parent>来声明继承的哪个父工程
上图就是此工程继承了spring-boot-starter-parent
- 综上,创建一个父工程,继承spring-boot-starter-parent父工程,并设置打包方式为pom,然后让子工程继承这个父工程
2)聚合
在继承当中,模块想要打包还需要去将父工程和其依赖的其他对应工程先打包下载到本地仓库后才能打包成功,在后面依赖关系多的情况下反而增加了工作量,因此采用了聚合操作。
聚合就是将多个模块组织成一个整体,同时进行项目的构建。
- 在父工程(聚合工程)中通过< modules>…< /modules>进行编写
这样操作后,当在父工程(聚合工程)中执行打包等操作的时候,聚合的模块都会同时执行相同的操作,就不用一个一个模块去执行操作。
2.JWT令牌(JSON Web Token)
json web token是一直开发标准,它定义了一种紧凑且包含的方式,用于各方之间以JSON对象的形式安全地传输信息。此消息可以被验证和信任,因为它是数字和签名的。JWT可以使用秘密(HMAC算法)或使用RSA或ECDSA的公钥/私钥进行签名。使用JWT最常见的场景是用户登录后,每个后续请求都将包含JWT,允许用户访问该令牌所允许的路由、服务和资源。
1)传统的登录认证流程:
- 用户首次登录成功并将用户信息保存到Session中()
- http协议本身是一种无状态的协议:用户成功登录之后继续发送后续业务请求,但是由于角色权限,用户每次发送后续请求都需要从Session中取出对应的用户信息来判断当前登录用户是否具有执行该业务的权限
2)JWT认证流程:
- 首先用户登录
- 后端核对用户名和密码成功后,将用户的id等其他信息作为JWT Payload(负载),将其与头部分别进行Base64编码拼接后签名,形成一个JWT(Token)。形成的JWT就是一个形同xxx.yyy.zzz的字符串,token==》head.payload.singurater
- 后端将JWT字符串作为登录成功的返回结果返回给前端。前端可以将返回的结果保存在localStorage或sessionStorage上,退出登录时前端删除保存的JWT即可。
- 前端在每次请求时将JWT放入HTTP Header中的Authorization位传递给后端服务器。(解决XSS和XRF问题)
- 后端统一拦截检查JWT是否存在,如存在验证JWT的有效性。例如,检查签名是否正确;检查Token是否过期;检查Token的接收方是否是自己。没有问题则放行
3)JWT组成:
- 第一部分:Header(头),记录令牌类型、签名算法等。
- 第二部分:Payload(有效载荷),携带一些自定义信息,默认信息等。
- 第三部分:Signature(签名),防止Token被篡改、确保安全性,将header、payload加入指定密钥,通过指定签名算法计算而来。
4)JWT使用:
-
导入jwt依赖
-
在yml文件中配置jwt属性值
-
创建jwt配置参数的封装类JwtProperties,来扫描yml配置文件的jwt属性,将其一一封装对应,以便于动态调用jwt属性值,实现解耦
/**
* jwt相关配置参数(admin-secret-key、admin-ttl、admin-token-name)的封装类
*/
// 声明为组件,能够被springboot自动装配到其他需要的地方
@Component
// 扫描yml配置文件的sky.jwt,让封装类属性与其对应
@ConfigurationProperties(prefix = "sky.jwt")
@Data
public class JwtProperties {
/**
* 生成jwt令牌相关配置
*/
private String adminSecretKey;
private long adminTtl;
private String adminTokenName;
}
- 创建生成和解析jwt令牌的工具类JwtUtil,这样在需要生成和解析jwt的时候通过引用jwt工具类传递相关配置参数调用对应方法。
/**
* 生成jwt令牌的工具类
*/
public class JwtUtil {
/**
* 生成jwt
* 使用Hs256算法, 私匙使用固定秘钥
*
* @param secretKey jwt秘钥
* @param ttlMillis jwt过期时间(毫秒)
* @param claims 设置的信息(一个map键值对,userID : id值),方便后面解析jwt然后获取id
* @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;
}
}
- 当用户登录成功,就生成jwt令牌(登录接口需要引用前面的参数配置封装类和jwt工具类)
@Autowired
private JwtProperties jwtProperties;
/**
* 用户登录
*/
@PostMapping("/login")
@ApiOperation("用户登录接口")
public Result<UserLoginVO> login(@RequestBody UserLoginDTO userLoginDTO){
log.info("员工登录:{}",userLoginDTO);
User user = userService.login(userLoginDTO);
// 登录成功,生成jwt令牌
Map<String, Object> claims = new HashMap<>();
// 保存登录用户id,然后每次验证通过的时候会解析jwt,然后获取id,将其存入线程,供后面调用
claims.put("userId",user.getId());
// 给jwt工具类传递参数(jwt密钥,jwt过期时间,设置的信息(用户id))
String token = JwtUtil.createJWT(
// 签名算法
jwtProperties.getAdminSecretKey(),
// 有效期
jwtProperties.getAdminTtl(),
// 自定义内容(载荷)
claims
);
UserLoginVO userLoginVO = UserLoginVO.builder()
.id(user.getId())
.mailbox(user.getMailbox())
.password(user.getPassword())
.token(token)
.build();
return Result.success(userLoginVO);
}
- 在jwt拦截作用中,首先我们需要创建一个起jwt令牌拦截器功能的类JwtTokenAdminInterceptor,他通过前端传递的token去调用jwt工具类解析出jwt,进行jwt验证,验证通过后根据原本登录时设置的值去获取用户id,进行用户id的保存,方便后期使用。
/**
* jwt令牌校验的拦截器,解析jwt,如果解析成功则通过(并将用户登录id保存到线程中)
*/
@Component
@Slf4j
public class JwtTokenAdminInterceptor implements HandlerInterceptor {
// 去依赖注入jwt的相关配置
@Autowired
JwtProperties jwtProperties;
/**
* 校验jwt
*/
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler){
// 判断当前拦截到的是Controller的方法还是其他资源
if(!(handler instanceof HandlerMethod)){
// 当前拦截到的不是动态方法,直接放行
return true;
}
// 1.从请求头中获取令牌
String token = request.getHeader(jwtProperties.getAdminTokenName());
// 2.校验令牌
try {
// 调用jwt工具类去解析jwt令牌,然后通过原本存入的用户名获取id,将其存入到线程内
Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(),token);
// 根据在登录过后保存claims的map键值对信息去获取id
Long empId = Long.valueOf(claims.get("userId").toString());
// 将从jwt里面获取的id存入到线程内,这样后面有地方需要用到登录用户的id时就可以直接从线程里面调用
// 为啥写进线程内?让id具有隔离性,当多个用户同时登录的时候不会产生数据重叠
BaseConstant.setCurrentId(empId);
// 3.通过,放行
return true;
}catch (Exception ex){
// 4.不通过,响应401状态
response.setStatus(401);
return false;
}
}
}
- 在配置类WebMvcConfiguration中去注册自定义拦截器,然后通过引用jwt拦截器JwtTokenAdminInterceptor,如果返回flase,那么就被拦截,解决没有登录就无法访问其他页面接口的功能。
// 去依赖注入jwt令牌校验的拦截器,返回ture通过,返回false不通过
@Autowired
private JwtTokenAdminInterceptor jwtTokenAdminInterceptor;
/**
* 注册自定义拦截器
*
* @param registry
*/
protected void addInterceptors(InterceptorRegistry registry){
log.info("开始注册自定义拦截器...");
registry.addInterceptor(jwtTokenAdminInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns("/user/login");
}
- 调用导入依赖工具包的API来完成JWT令牌的生成
//登录成功后,生成jwt令牌
Map<String, Object> claims = new HashMap<>();
// 存入id
claims.put("id",1);
claims.put("username","Tom");
String token = Jwts.builder()
// 签名算法和密钥
.signWith(SignatureAlgorithm.HS256,"it"))
// 有效期
.setExpiration(new Date(System.currentTimeMillis() + 12*3600*1000))
// 自定义内容(载荷)
.setClaims(claims)
.compact();
System.out.println(token);
- JWT令牌的校验
public static Claims parseJWT(String token){
Claims claims = Jwts.parser()
.setSigningKey("it")
.parseClaimsJws(token)
.getBody();
return claims;
}
3. Nginx反向代理
前端发送的动态请求由nginx转发到后端服务器
1)Nginx能帮助我们做什么?
- 正向代理
- 反向代理
- 动静分离
- 负载均衡
- 集群高可用
4.Yapi
Yapi是设计阶段使用的工具,管理和维护接口,Swagger是在开发阶段使用的框架,帮助后端开发人员做后端的接口测试
5.Swagger(帮助后端生成接口文档和接口测试)
- 使用Swagger只需要按照它的规范去定义接口及接口相关的信息,就可以做到生成接口文档,以及在线接口调试页面。
- 官网:https://swagger.io
- 但直接使用较为繁琐,所以使用Knife4j为Java MVC 框架集成Swagger生成Api文档的增强解决方案。
1)Swagger使用方式
- 导入Knife4j的maven坐标
- 在配置类(WebMvcConfiguration)中加入Knife4j相关配置
- 设置静态资源映射,否则接口文档页面无法访问
2)Swagger常用注解
6.ThreadLocal
ThreadLocal 为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问,每次请求就是一个单独的线程(例如你添加用户,请求发出后,拦截器jwt令牌的验证、service等层次的代码属于一个线程)。
1)ThreadLocal常用方法
2)ThreadLocal作用
在我们需要获取当前登录者的相关信息时(如id),我们可以通过jwt令牌里面存入的信息来得到。通过jwt令牌解析得到登录者id后怎么在其他代码中获取了?那么就运用到了ThreadLocal,因为用户的每一次请求都是单独的一个线程,那么我们可以在这一次请求中将id存入到线程里面,然后在需要id的代码处从线程里面获取出来(在拦截器验证的时候从jwt令牌里面获取id,然后将其存入到线程空间里面,后面在service层中获取线程里面的id),由此便解决了问题。
7.处理公共字段自动填充(如:添加和修改表的的创建者、创建时间、修改者、修改时间等)
每次在表中增加数据或修改数据的时候,我们还需要为对象进行创建者、创建时间、修改者、修改时间等数据的赋值,但是在项目中这样会增加代码量.
如何进行简化?随之我们想到aop切面,对这中公共字段统一进行一个处理,这样就不用每次都去进行赋值操作;
那么aop拦截的条件是什么?通过观察我们发现,并不是每一个操作都需要进行上面四个字段的赋值,当数据库mapper操作类型为insert、update时才会进行,所以我们就以这两个操作类型为拦截条件;
aop和数据库mapper之间并没有关联,我们该如何进行一个拦截,怎么去识别它的操作类型?这里运用到了自定义注解的知识,我们自定义一个注解,然后在mapper方法上加入自定义注解,这样aop就能识别并进行一个拦截;
拦截后我们增强处理(赋值)怎么做?因为在赋值时,每一次的对象都可能是不同的,这一次是对用户表进行操作,下一次可能就是对图片表进行操作,我们并不能去确定它是使用的哪一个对象,无法直接去调用对象进行赋值,进而想到了反射,我们利用反射去获取到对象并对其进行字段的赋值。技术点:枚举、注解、AOP、反射
自定义注解
1)创建enum类型的数据库操作OperationType,通过调用声明或获取数据库操作类型
2)通过我们1)定义的操作类型,去创建一个自定义注解接口AutoFill,让注解内容为我们1)定义的类型。
/**
* 自定义注解,用于标识某个方法需要进行字段自动填充处理
* 可以使用此注解来声明该方法的数据库操作类型,这样在AOP操作
* 的时候获取到该注解,通过反射可以得到该注解的内容
*/
// 要求注解只能注解在方法上面
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
// 声明注解的类型(我们定义的数据库操作类型:UPDATE INSERT)
OperationType value();
}
3)加入切面类中需要用到的切面相关的依赖
4)自定义切面类AutoFillAspect,统一拦截加入了AutoFill注解的方法,通过反射为公共字段赋值
/**
* 自定义切面,实现公共字段自动填充处理逻辑
*/
// 声明切面
@Aspect
// 让容器管理的bean
@Component
// 日志
@Slf4j
public class AutoFillAspect {
/**
* 切入点(对哪些类的哪些方法进行拦截)
* execution(* com.sky.mapper.*.*(..))表示拦截在该包下的所有接口和类里面的所有方法
* @annotation(com.sky.annotation.AutoFill) 表示拦截使用过AutoFill自定义注解的方法
* 两个结合起来就表示拦截该包下面使用过自定义注解的方法
*/
@Pointcut("execution(* com.example.zhijingai.mapper.*.*(..)) && @annotation(com.example.zhijingai.demo.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];
//准备赋值的数据,从保存id的类中获取
LocalDateTime now = LocalDateTime.now();
Long currentId = BaseConstant.getCurrentId();
//根据当前不同的操作类型,为对应的属性通过反射来赋值
if(operationType == OperationType.INSERT){
//为4个公共字段赋值
try {
Method setCreateTime = entity.getClass().getDeclaredMethod("setCreateTime", LocalDateTime.class);
Method setCreateUser = entity.getClass().getDeclaredMethod("setCreateUser", Long.class);
Method setUpdateTime = entity.getClass().getDeclaredMethod("setUpdateTime", LocalDateTime.class);
Method setUpdateUser = entity.getClass().getDeclaredMethod("setUpdateUser", Long.class);
//通过反射为对象属性赋值
setCreateTime.invoke(entity,now);
setCreateUser.invoke(entity,currentId);
setUpdateTime.invoke(entity,now);
setUpdateUser.invoke(entity,currentId);
} catch (Exception e) {
e.printStackTrace();
}
}else if(operationType == OperationType.UPDATE){
//为2个公共字段赋值
try {
Method setUpdateTime = entity.getClass().getDeclaredMethod("setUpdateTime", LocalDateTime.class);
Method setUpdateUser = entity.getClass().getDeclaredMethod("setUpdateUser", Long.class);
//通过反射为对象属性赋值
setUpdateTime.invoke(entity,now);
setUpdateUser.invoke(entity,currentId);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
5)在相关Mapper的方法上加入AutoFill注解
AOP:
反射:
3)在Mapper的方法上加入AutoFill注解
8.文件上传——阿里云OSS
1)阿里云OSS使用步骤
2)文件上传代码实现步骤
- 创建通用接口类,其中编写文件上传方法接口
/**
* 通用接口
*/
@RestController
@RequestMapping("/admin/common")
@Api(tags = "通用接口")
public class CommonController {
// 依赖注入
@Autowired
AliOssUtil aliOssUtil;
// 图片文件上传接口
@PostMapping("/upload")
@ApiOperation("文件上传")
public Result<String> upload(MultipartFile file){
try {
// 获取原始文件名
String originalFilename = file.getOriginalFilename();
// 截取原始文件名的后缀 ddd.png (在最后一位小数点的后面)
String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
// 通过UUID随机生成文件名
String objectName = UUID.randomUUID().toString() + extension;
// 根据工具类设定好文件访问路径网址
String filePath = aliOssUtil.upload(file.getBytes(),objectName);
return Result.success(filePath);
}catch (IOException e){
e.printStackTrace();
}
return null;
}
}
- 将文件上传到阿里云OSS中去
①将地域节点、访问凭证、Bucket名称等属性添加进配置文件
②创建一个封装类去封装地域节点、访问凭证、Bucket名称等属性,采用@ConfigurationProperties(prefix=“sky.alioss”)注解扫描yml文件对应配置属性值(使用这个注解SpringBoot会将配置文件yml中sky.alioss设置的值一一与属性对应赋值)
③创建阿里云OSS工具类(执行文件上传并返回保存后图片访问路径),这样接口就可以直接访问工具类得到最终保存的图片路径就行。
④为了减少耦合度,所以不能直接在阿里云OSS工具类里面写死属性值。于是我们需要创建一个配置类通过②的封装类去创建工具类。当程序启动时加载此配置类就创建文件工具类。
3. 因为工具类在配置类中被创建,所以执行启动后就创建了工具类对象,将工具类依赖注入到接口类后直接在文件上传接口中去引用文件上传工具类对象,返回设定好的图片访问路径。
9.Redis
redis是一个基于内存的key-value结构数据库
- 基于内存存储,读写性能高
- 适合存储热点数据(在某个特定的时间点有大量的用户访问)
1.Redis下载与安装
启动redis
2.Redis数据类型
3.Redis常用命令
3.1
3.2
3.3
3.4
3.5
3.6
4.在Java中操作Redis
4.1 Spring Data Redis使用方式
③编写配置类
④通过RedisTemplate对象操作Redis
10.HttpClient
11.微信小程序
微信小程序目录结构:
微信登录