阶段项目 – 智能学习辅助系统
1. 开发流程
查看页面原型,明确需求;
阅读接口文档;
思路分析;
接口开发;
即代码实现,要严格遵守接口文档
如:接口文档规定的请求路径、请求方式…
接口测试;
先通过postman这样的接口测试工具进行测试
前后端联调
启动前后端,访问前端工程,通过前端工程访问服务端接口。
*. 打开nginx(占用90端口),找到部署的前端工程;
打开要测试的后端部分,触发功能(即发送请求);
nginx接收请求后,会将请求发送给8080端口的tomcat,由tomcat处理请求,将数据返回给前端;
此处tomcat处理请求流程即为下方“功能实现流程”。
前端解析后将数据渲染展示。
2. 功能实现流程
-
前端发送请求后会请求到controller的方法;
-
controller(控制层)中先调用service(业务逻辑层)获取数据;
-
service(业务逻辑层)中调用mapper(数据访问层)接口中的方法;
-
mapper(数据访问层)调用实现类中的方法向数据库发送sql语句,并将结果封装,最后返回给service,service再返回给controller,controller拿到数据后再返回给前端。
在这里,数据访问层为“mapper”,使用Mybatis这一款优秀的持久层框架。
3. 工具使用
-
lombak,简化了JavaBean类的方法书写,使代码更加简洁;
导入依赖:
<!--lombok--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h1EdGugm-1682127453605)(D:\a05_JavaWeb\我的笔记\图\lombak使用.png)]
-
pageHelper,进行分页条件查询
4. 登录校验 - 令牌技术
通过如下步骤可以完成登录校验的操作(JWT令牌技术+过滤器Filter):
- 客户端向服务端发送请求,服务端生成令牌下发给客户端;
- 客户端每次请求中携带令牌至服务端,服务端对请求进行统一拦截,并获取请求中的令牌进行检验。
4.1 会话跟踪方案
传统会话跟踪技术:
- 客户端会话跟踪方案:Cookie;
- 服务端会话跟踪方案:Session;
主流方案:
- 令牌技术。
4.2 令牌技术
为达到登录校验的目的,我们对主流的令牌技术进行使用。
JWT( JSON Web Token):将原始的JSON格式进行了封装,实现了加密的效果。
组成:
第一部分:Header(头),记录令牌类型、签名算法等;
第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等;
第三部分:Signature(签名):防止Token被篡改,确保安全性。
*前两部分使用Base64编码(基于26个字母的大小写、0-9、+、/来表示二进制数据的编码方式)。
-
引入依赖;
<!--JWT令牌--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency>
-
在工具类中书写生成和解析JWT令牌的方法;
//生成JWT令牌 private static String signKey = "offer888"; private static Long expire = 43200000L; public static String generateJwt(Map<String, Object> claims){ String jwt = Jwts.builder() .signWith(SignatureAlgorithm.HS256, signKey) //签名算法和密钥 .addClaims(claims) //自定义内容(载荷) .setExpiration(new Date(System.currentTimeMillis() + expire)) //设置有效期 .compact(); return jwt; } //解析JWT令牌 public static Claims parseJWT(String jwt){ Claims claims = Jwts.parser() .setSigningKey(signKey) //生成令牌时使用的密钥 .parseClaimsJws(jwt) //令牌 .getBody(); return claims; //返回自定义信息 }
-
Controller层:利用用户名和密码条件查询表中是否存在该用户进行登录验证;
返回用户对象,若返回对象非空,代表该用户存在且密码正确,下发令牌;
使用Map集合存储自定义信息,调用方法生成令牌并返回。
@PostMapping("/login") public Result login(@RequestBody Emp emp){ log.info("用户登录 -> 账号:{};密码:{}", emp.getUsername(), emp.getPassword()); Emp e = empService.login(emp); //登录成功,生成令牌,下发令牌 if(e != null){ Map<String, Object> claims = new HashMap<>(); claims.put("id", e.getId()); claims.put("name", e.getName()); claims.put("username", e.getUsername()); String jwt = JwtUtils.generateJwt(claims); return Result.success(jwt); } //登陆失败,返回错误信息 return Result.error("用户名或密码错误"); }
-
Service层:书写方法调用mapper层的方法;
-
Mapper层:根据用户名和密码查询员工。
@Select("select * from emp where username = #{username} and password = #{password}") Emp getByUsernameAndPassword(Emp emp);
5. 登录校验 - 过滤器Filter
JavaWeb三大组件(Servelet、Fileter、Listener)之一。
过滤器一般完成一些通用的操作,如:登录校验、统一编码处理、敏感字符处理等。
*Servelet、Listener目前使用较少。
-
创建一个类实现Fileter接口;
-
类上添加注解@WebFileter(***)配置拦截路径;
配置方式 举例 拦截具体路径 /login 目录拦截 /emps/* 拦截所有 /* -
Fileter并不是SpringBoot中的组件,使用需要在启动类上添加注解@ServeletComponentScan。
代表开启了对servlet组件的支持
-
下面是登陆检验使用过滤器的代码实现:
@Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) servletRequest; HttpServletResponse resp = (HttpServletResponse) servletResponse; // 1.获取请求url String url = req.getRequestURL().toString(); log.info("请求的url:{}", url); // 2.判断请求url中是否包含login,如果包含,说明是登录操作,放行 if(url.contains("login")){ log.info("登陆操作,放行..."); filterChain.doFilter(servletRequest, servletResponse); return; } // 3.获取请求头中的令牌(token) String jwt = req.getHeader("token"); // 4.判断令牌是否存在,如果不存在,返回错误结果(未登录) if(!StringUtils.hasLength(jwt)){ log.info("请求头token为空,返回未登陆的信息"); Result error = Result.error("NOT_LOGIN"); //手动转换 对象->JSON ----->使用阿里巴巴fastjson String notLogin = JSONObject.toJSONString(error); resp.getWriter().write(notLogin); //调用resp中的输出流getWriter(),再调用write()将信息响应回去 return; } // 5.解析token,如果解析失败,返回错误结果(未登录) try { JwtUtils.parseJWT(jwt); } catch (Exception e) { e.printStackTrace(); log.info("解析令牌失败,返回未登录错误信息"); Result error = Result.error("NOT_LOGIN"); //在Controller中可以自动转换为JSON格式 //在这里需要手动转换 对象->JSON -----> 使用阿里巴巴fastjson String notLogin = JSONObject.toJSONString(error); resp.getWriter().write(notLogin); return; } // 6.放行 log.info("令牌合法,放行"); filterChain.doFilter(servletRequest,servletResponse); }
6. 登录校验 – 补充
在登录校验使用过滤器时,因为不是在Controller层中,获取到的对象不会自动转换为JSON格式,因此我们引入了阿里巴巴fastjson进行手动转换。
阿里巴巴fastjson使用:
-
在pom文件中引入阿里巴巴fastjson的依赖;
<dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.76</version> </dependency>
-
使用时调用方法即可。
7. 文件上传
7.1 简介
7.1.1 文件上传的前端页面三要素
-
表单项 type=“file”;
设置此表单项在页面中就出现了”选择文件“的选项,可以选择文件上传。
-
表单提交方式 post;
使用post提交方式,因为上传的文件一般都比较大
-
表单的enctype属性(默认方式提交的只是文件名–url编码后的格式)。
使用enctype属性指定上传方式为multipart/form-data,因为普通默认的编码格式不适合上传大型的二进制数据
7.1.2 服务端接收数据
在服务端定义方法接收即可,在方法中声明形参;对于文件,Spring提供了一个API – MultipartFile,使用该API就可以接收上传的文件。
要成功接收上传的文件,要保证表单项的名称和方法形参的名称保持一致;若不一致可通过@RequestParam注解指定Value属性。
7.2 本地存储
在服务端,接收到上传上来的文件之后,将文件存储在本地服务器的磁盘中。
构造唯一的文件名,调用transferTo方法
@PostMapping("/upload")
public Result upload(String username, Integer age, MultipartFile image) throws IOException {
log.info("文件上传:{}, {}, {}", username, age, image);
//获取原始文件名
String originalFilename = image.getOriginalFilename();
//构造唯一的文件名(文件名不能够重复)
int index = originalFilename.lastIndexOf(".");
String exname = originalFilename.substring(index);
String newFileName = UUID.randomUUID().toString() + exname;
log.info("新的文件名:{}", newFileName);
//将文件存储在服务器的磁盘目录中 D:\图集\学习案例
image.transferTo(new File("D:\\图集\\学习案例\\"+newFileName));
return Result.success();
}
7.3 补充
SpringBoot中,默认上传单个文件大小为1M,我们可以在配置文件中进行大小的设置:
#文件上传大小配置 servlet: multipart: max-file-size: 10MB #对单个文件上传大小的设置 max-request-size: 100MB #对单次文件上传大小的设置
8. 文件上传 – 使用阿里云OSS
起初使用本地存储的形式进行文件上传,本地存储有几项缺点:无法直接访问,磁盘空间限制,受磁盘状态影响(如磁盘损坏)。
现使用阿里云OSS存储文件。
PS.篇幅较长,在另一篇文章中做详细分析。
9. 记录操作日志
9.1 需求
将增、删、改相关接口的操作日志记录到数据库表中。
9.2 实现思路
对所有业务类中增、删、改方法添加统一功能,使用AOP技术最方便。
增、删、改方法没有规律,故可使用自定义注解的方式完成目标方法的匹配。
@Retention(RetentionPolicy.RUNTIME) //注解作用场景
@Target(ElementType.METHOD) //注解作用位置
public @interface Log {
}
9.3 步骤
-
在数据库中添加日志表;
IDEA中要刷新数据库连接,连接到新表。
-
创建OperateLog类;
-
定义注解@Log;
-
定义OperateLogMapper接口,写入插入日志数据的代码;
10. 出现过的错误
-
不该出现的错误:使用预编译sql,语句中‘#{}’中没有填写参数;
不知道为什么对其他方法进行了报错,排查方向出现问题,耗费两个半小时!
*知道为什么对其他方法进行报错了,在其他模块中有本模块相同的方法名,导致idea红色报错,但是这种报错不影响SpringBoot程序的启动。
*细节
其实很多都是注解的使用。
-
控制层、业务逻辑层、数据访问层的class或接口实现类的文件类名上要标注注解;
controller(控制层):@RestController
//@RestController=@Controller+@ResponseBody //@Controller: 表明该类是一个控制类 //@ReponseBody: 会将返回值的对象转为json再响应回来
service(业务逻辑层):@Service
mapper(数据访问层):@Mapper
-
控制层、业务逻辑层内要添加使用依赖注入的注解@Autowired;
控制层要调用业务逻辑层的方法,故在控制层内创建业务逻辑层接口名的对象,并添加依赖注入的注解;
业务逻辑层要调用数据访问层的方法获取数据库的数据,故在业务逻辑层创建数据访问层的对象,并添加依赖注入的注解。
*上述注解使用的前提是各层已将bean对象交给IOC容器管理。
-
在控制层类名上添加注解@Slf4j,该注解即使用日志,可以在方法内直接调用log.info(),在括号内书写日志;
使用日志书写相比于”sout“更加专业。
-
请求路径:添加注解@Mapping,写于方法体之上,在之后书写请求路径,用于表示调用什么方法完成什么操作;
可在注解前加上请求方式,如:
// 查询数据 @GetMapping("/depts") //限定了请求方式为GET,请求路径为"/depts" public Result list() { log.info("查询全部部门数据"); //使用日志,更加专业 List<Dept> deptList = deptService.list(); //获取业务逻辑层返回的数据 return Result.success(deptList); } // 删除数据 @DeleteMapping("/depts/{id}") //限定了请求方式为DELETE,请求路径为"/depts/{id}" public Result delete(@PathVariable Integer id){ log.info("根据id删除部门:{}", id); //调用service删除部门 deptService.delete(id); return Result.success(); } /*此处一个注意点,使用了根据id删除数据的方式,请求路径添加了“/{id}”。 因此在书写方法形参时,要在形参前添加注解@PathVariable,由此会将方法的 实参传递给请求路径,替换掉占位符,获得完整的请求路径。*/
*小细节
-
程序错误,修改后要重新启动程序,再去试验是否修改正确;
-
报错404:请求资源不存在(URL有误或资源删除);
修改错误:
- 可能是自己程序书写错误,如:sql语句属性名书写错误等;
-
在mapper中书写较长的SQL时,可以创建一个XML映射文件:
下面对XML映射文件举例:
<!--书写配置--> <?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"> <!--此处注意点:namespace、id、resultType--> <mapper namespace = "com.itheima.mapper.EmpMapper"> <select id="list" resultType="com.itheima.pojo.Emp"> select * from emp <where> <if test="name != null and name != ''"> name like concat('%', #{name}, '%') </if> <if test="gender != null"> and gender = #{gender} </if> <if test="begin != null and end != null"> and entrydate between #{begin} and #{end} </if> </where> order by update_time desc </select> <!--在这里,对多个id对应的数据进行删除操作,使用了foreach进行遍历集合--> <delete id="delete"> delete from emp where id in <foreach collection="ids" item="id" separator="," open="(" close=")"> #{id} </foreach> </delete> </mapper>
有几点注意事项:
-
创建于”resources“包,与对应的Mapper接口同包同名;
-
首先书写配置,在Mabatis中文网的”入门“标签下可找到;
-
XML映射文件中的namespace属性与Mapper接口全类名保持一致;
namespace => 接口全类名
-
XML映射文件中SQL语句的id与Mapper接口中的方法名一致,并保持返回类型一致。
id => Mapper接口中的方法名;
resultType => 方法的返回类型(返回类型为单条记录所封装的类型)
-
reach collection=“ids” item=“id” separator=“,” open=“(” close=“)”>
```#{id} </foreach> </delete>
有几点注意事项:
-
创建于”resources“包,与对应的Mapper接口同包同名;
-
首先书写配置,在Mabatis中文网的”入门“标签下可找到;
-
XML映射文件中的namespace属性与Mapper接口全类名保持一致;
namespace => 接口全类名
-
XML映射文件中SQL语句的id与Mapper接口中的方法名一致,并保持返回类型一致。
id => Mapper接口中的方法名;
resultType => 方法的返回类型(返回类型为单条记录所封装的类型)