《项目案例—黑马tlias智能学习辅助系统》
目录:
- 学习目标
- 前端页面开发
- 后端环境搭建
- 后端部门管理功能开发(查询部门、删除部门、新增部门、修改部门)
- 后端员工管理功能开发(查询员工、删除员工、新增员工、修改员工、文件上传、文件存储)
- 登录验证功能开发(登录校验,令牌技术,Filter过滤器统一拦截(JSON数据封装响应),interceptor拦截器(定义、注册配置))
- 统一异常处理
- 事务处理
- AOP
- 附录:yml配置、手机访问项目
一:学习目标:
1)后端开发:根据老师提供的接口开发文档,完成对应功能模块接口的开发(部门管理、员工管理的增删改查、阿里云文件上传、Springboot的yml配置、页面登录验证),了解日常工作中的开发流程顺序;
2)前端优化:学有余力的情况下,对比参考老师提供的前端页面进行修改;
3)手机端访问项目;
页面原型:
二、前端页面开发:由老师提供了前端页面。也可以自己写。
1)老师提供的界面(目前是使用老师写的):
2)前面自己写的…(一坨答辩)–后面开发完成后,需要自己参照着加强前端页面的开发…
三、后端环境搭建:
理清开发顺序:先理解开发需求—整理开发文档—搭建开发环境—根据开发文档进行功能开发;
1)开发需求+开发文档(老师提供):
页面原型和接口文档有具体的开发要求;
2)搭建环境:
1.建立Springboot项目,三层架构:
项目结构如图:
2.数据库准备:
DDL(定义数据):建立一个tlias数据库;建表:dept(部门表)、emp(员工表);插入数据
四、部门管理功能开发–四个功能(查询部门、删除部门、增加部门、编辑部门):
1)查询部门:
2)删除部门:
功能类似,Sql稍稍不同,不再赘述;
注意点:
- 前端传过来的可能是路径参数,在Controller层需要用@PathVariable注解去接收:
- mapper中,使用到的数据,需要预编译Sql(防止Sql注入),需要用#{ }的方式
3)新增部门:
功能类似,Sql稍稍不同,不再赘述,需要注意的是,发送过来的数据较多,往往会封装为一个对象,返回数据,也需要封装为一个对象返回。这时候需要使用@RequestBody注解来处理前端发送过来的数据;
4)修改部门:
功能类似,Sql稍稍不同,不再赘述;
五、员工管理的功能开发–六个功能(查询员工、删除员工、新增员工、修改员工、文件上传、文件存储):
因为员工查询的数据处理较多,这里员工功能接口统一用Mybatis的xml文件映射器来处理sql,与部门管理使用注解的方式不同;
1)查询员工:
查询员工的比较复杂,会涉及到分页查询和条件查询。分页查询有两种解决方法:1.调用两次数据库查询,分别查出总记录数和员工列表;2.利用分页插件PageHelper来完成;
方法1:
Xml映射文件:
ResultType代表的是单条记录返回的类型;
方法2:使用PageHelper插件。
使用PageHelper的原因是,目前只是一个简单案例,就一个Emp表,需要分页查询就得定义两条查询Sql,如果后面很多表的,那每个表的分页查询都得这么写,并且还封装到PageBean中,那就很麻烦了,所以考虑引入PageHelper插件;
方法二和方法一Controller、Service、Mapper层一样,Service实现类和xml文件不一样,xml文件不用再写limit了;
在xml文件中,要把limit这一句给删掉;
注意点:
1)要注意看接口文档,发送的数据不是Json格式,而是直接以get方式来发送,需要全部接收的。不能以JSON对象的写法来接收发过来的参数,不然会提示异常:HttpMessageNotReadableException 是 Spring Framework(特别是 Spring Web MVC 和 Spring Boot 应用程序中)处理 HTTP 请求时可能会抛出的一个异常。这个异常通常发生在 Spring 尝试将接收到的 HTTP 请求消息体转换为 Java 对象时,例如使用 @RequestBody 注解的方法参数时,如果无法正确解析或读取消息内容,就会抛出此异常。
以下是可能导致 HttpMessageNotReadableException 的一些常见原因:
1.请求体为空:当一个方法通过 @RequestBody 注解期待接收请求体中的数据,但实际请求中并没有提供任何有效的内容。
2.请求体格式不正确:如果客户端发送的数据格式与控制器期望接收的数据类型(如 JSON、XML 等)不符,或者数据结构不符合对应的 Java 类型映射规则。
数据内容本身有语法错误:例如 JSON 格式不完整或包含非法字符。
3.后端的实体类与前端的传入参数数据不一致:如果后端定义的实体类(如 Java 类的字段和类型)与前端发送的数据不匹配,可能会导致反序列化失败。
4.请求方式不正确:如果使用了 @RequestBody 注解但请求方法是 GET(GET 请求通常不包含请求体),那么应该使用 POST 或其他支持请求体的方法。
5.跨域问题:如果后端被 @RequestBody 修饰的参数实体类没有实现序列化接口,或者没有正确解决跨域问题,也可能导致此异常。
6.请求体过大:如果请求体的大小超过了服务器配置的限制大小,也可能触发这个异常。
解决这个问题的方法取决于具体的情况,可能需要检查请求体数据、检查后端代码和配置、确保前后端数据格式一致、调整请求方式、解决跨域问题等。
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">
<mapper namespace="com.itheima.mapper.EmpMapper">
<!-- 查询用户列表 -->
<select id="queryEmpList" resultType="com.itheima.pojo.Emp">
-- 这里查询出来不就全是emp吗?resultType返回的类型应该是什么?
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
limit #{page},#{pageSize}
</select>
<!-- <!– 根据 ID 查询用户 –>-->
<!-- <select id="selectUserById" resultType="com.example.domain.User" parameterType="int">-->
<!-- SELECT * FROM user WHERE id = #{id}-->
<!-- </select>-->
<!-- <!– 插入用户 –>-->
<!-- <insert id="insertUser" parameterType="com.example.domain.User">-->
<!-- INSERT INTO user (name, email, phone) VALUES (#{name}, #{email}, #{phone})-->
<!-- </insert>-->
<!-- <!– 更新用户 –>-->
<!-- <update id="updateUser" parameterType="com.example.domain.User">-->
<!-- UPDATE user SET name = #{name}, email = #{email}, phone = #{phone} WHERE id = #{id}-->
<!-- </update>-->
<!-- <!– 删除用户 –>-->
<!-- <delete id="deleteUser" parameterType="int">-->
<!-- DELETE FROM user WHERE id = #{id}-->
<!-- </delete>-->
</mapper>
resultType的返回写法:简单来说,就是返回的每一行数据是啥,就要返回啥;
2)删除员工:
和前面的删除部门没有太大区别,关键在于按照接口文档,可能是会传批量删除的数据过来(是一个数组Id),这就需要遍历执行删除操作。两个遍历选择:
1.在service层,循环执行java代码;【有个问题就是需要和前端确定传入的格式是int类型数组】
2.在xml文件中,执行循环:使用foreach标签
3)新增员工:
和新增部门代码、逻辑类型,不再赘述代码细节;
1)多了一点是,要增加文件上传的功能;
2)新增员工,如果员工表是主键自增的,有可能需要拿到自增的id(主键),用于后续的逻辑处理,印象中是有一个注解的
4)修改员工:
不再赘述;
1)需要先调用一次查询,查询回显数据;
2)前端页面修改了数据后,再更新数据;
5)文件上传:
1.前端页面设计页面上传的编写,有三个要求:
- 提交方式必须为post;
- 必须要有有file这样的表单项;
- 必须使用这个属性:enctype=“multipart/form-data”
2.服务端想要接收到,还是和之前一样,定义controller,然后想要接收到文件,需要使用Springboot的一个接口:MultipartFile
3.通过debug可以查看,文件上传后,会在磁盘生成.tmp后缀名的临时文件,执行完成后,会自己删掉;所以如果需要保存下来,可以对这些文件进行操作(有多少个文件,取决于你的前端页面的内容),涉及到了存储方案。
4.文件存储
1)磁盘本地存储:(容易收到硬件大小的限制,且要自己做好备份和容灾部署)
0.先获取上传的文件后缀;
1.为了防止文件名重复,利用UUID设置唯一文件名:
2.利用MultipartFile的方法:transferTo写入磁盘:
注意:Springboot有文件上传大小的限制,需要自己去properties中去调整设置,否则会报错
#配置单个文件最大上传大小
spring.servlet.multipart.max-file-size=10MB
#配置单个请求最大上传大小(一次请求可以上传多个文件)
spring.servlet.multipart.max-request-size=100MB
2)云存储:使用别人公司的云服务(需要自己支付一定的云服务费用)
例如:如果需要使用短信服务,自己写就得自己去对接各大短信运营商;或者调用阿里/腾讯这些的短信云服务,省去自己配置的麻烦;
打开官方的SDK示例:
配置步骤和老师视频有些出入:
1)打开示例文档中的简单上传–文件上传
2)需要AccessKeyId,往回看文档,发现需要配置Java访问凭证
有多种方式,这里选择STS临时凭证的方式
按照步骤来配置:
1.先创建用户,为用户授权、创建角色、为角色配置自定义文件上传策略
2.麻烦的是第三步,文档写得不清不楚,需要调用接口,又不给例子,后来才理解要自己写示例代码,在idea中生成访问凭证的securityToken,accessKeyId、accessKeySecret;
详见视频:https://help.aliyun.com/zh/oss/developer-reference/use-temporary-access-credentials-provided-by-sts-to-access-oss?spm=api-workbench.Troubleshoot.0.0.533a7185b4T8Cu#section-5xa-zdn-s0q
忍不住想吐槽,写得位置不一样,难受,卡了好久
在Idea中仿照示例,写生成securityToken,accessKeyId、accessKeySecret的测试代码,执行后,生成这三个:
3.最后,再写一个上传的类,记得这里的securityToken,accessKeyId、accessKeySecret要用生成的:
上传成功:
项目集成文件上传:
文件存储步骤已经清楚,那就需要与自己的项目集成。
1)构建工具类,写的是仿照官方文件上传示例代码(上面的Demo)的修改版本,并把这个类交给IOC管理。
2)按照接口文档,写上传的controller,返回url给用户:
3)联调,前端能够顺利调用,阿里云平台也有数据:
六、登录验证功能开发:
在项目中的登录是严格的;需要输入密码后才能登录,并且后续的所有请求都要带上令牌
1)登录功能
新建一个controller(之所以要新建一个controller,controller对应的其实是一个新的请求,而且不归属于前面的内容,所以新建一个controller)。
注意:
在MVC(Model-View-Controller)架构中,controller负责处理用户的请求,并根据这些请求执行相应的业务逻辑,然后将结果传递给视图进行展示。因此,controller的定义应该与应用程序的业务逻辑或功能模块相对应)
2)登录校验
之所以要有登录校验,是因为前面写的功能都没有添加校验的,那就是没登录,但是只要知道了请求路径,也是可以直接进入界面,这样就不是很好了。
HTTP是无状态的,意思是每一次请求都是独立的
为了起到登录校验的效果,在每一次请求的时候,都得发送一个标记,说明已经登录过了,然后设置统一拦截,所有请求都得经过这个关卡。
有三种技术:
1.会话技术:所谓会话,就是每一次打开浏览器,然后服务器也没有断开,这之间的通信就是一次会话,一次会话中可以包含多次请求和响应
会话跟踪:服务器需要识别对此请求是否来自同一个浏览器,然后便于后续的多次请求共享数据,重点是共享数据。
会话跟踪方案:
- 客户端会话跟踪技术:Cookie
1)cookie是存储在客户端/浏览器的;
2)cookie应用一般分三步:第一步向服务器发起请求时,服务器会自动设置cookie,然后在第二步浏览器再次发起请求时,会自动携带上cookie去请求,第三步,服务器可以获取到再次发送的请求cookie
Cookie存储的位置:
关于服务器或浏览器关闭后,cookie是否会自动删除的问题,我们可以从以下几个方面来详细解答:
一、服务器关闭后
当服务器关闭时,它并不会直接影响客户端(如浏览器)上的cookie。这是因为cookie是存储在客户端浏览器上的,而非服务器上。服务器关闭后,已经发送到客户端的cookie仍然会保留在客户端上,直到它们满足被删除的条件。
二、浏览器关闭后
对于会话cookie(Session Cookie):这类cookie在默认情况下,如果没有设置过期时间,那么它们将在用户关闭浏览器后自动删除。也就是说,这类cookie仅在浏览器打开期间有效,一旦关闭浏览器,这些cookie就会被清除。
对于持久化cookie(Persistent Cookie):如果设置了过期时间(通过setMaxAge()设置为正数),那么即使浏览器关闭,cookie也不会被删除,而是在指定的存活时间到期后才会被浏览器自动销毁。
服务端会话跟踪技术:Session
1)Session和cookie不一样,是存储在服务器的;第一步,发送请求,然后会在服务器生成JsessionId,第二步,响应回去给浏览器;第三步,浏览器再次发送请求,携带上对应的JSessionId
2)Session其实解决不了跨域的问题;那就是集群的时候不好处理
令牌技术
1)所谓的令牌,其实没有那么神秘,就是一个字符串。逻辑和cookie、session差不多,也是分三步,1.发送请求,然后在服务器生成一个字符串(我们称之为令牌);2.响应回浏览器,浏览器存储起来,这时候不一定是存储在cookie空间,哪里都可以存储,有可能是本地空间等;3.再次请求,携带上这个令牌,名称以token:XXX的方式再次请求
2)使用方式
1.先引入依赖
<!-- JWT令牌-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
2.现在测试中测试没问题了,然后可以抽成一个工具类,后续要集成使用,就再使用就好了
3.过滤器Filter
需要注意的是,Filter是javaWeb的,所以你要用Filter就得自己写类,写拦截方法。想要在Springboot项目中使用Filter,需要这几步:
Springboot有它的扫描机制,只会扫描被Spring框架管理的bean,而filter其实不是属于Springboot的,而是属于javaWeb的,所以你及时放在同级包下也没有用,如果要用就得在Springboot启动类中加上@ServletComponentScan注解,这个注解可以用于自动注册实现了@WebFilter、@WebListener或@WebServlet等Servlet 3.0+规范中定义的注解的组件。
2.filter放行的操作是:
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("filter拦截了");
filterChain.doFilter(servletRequest,servletResponse);
System.out.println("放行了,走吧");
}
3.filter拦截路径:
如果配置了多个过滤器,有形成过滤器链,有先后顺序,过滤器执行顺序是一个栈,先校验,后放行。两种方式设置过滤器优先顺序
1)什么都不管,就会以过滤器名称排序
2)在@WebFilter里加入order,order值越小,越先执行该过滤器
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
@WebFilter(urlPatterns = "/*", order = 1) // 设定顺序为1
public class MyFirstFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
System.out.println("执行 MyFirstFilter...");
chain.doFilter(request, response); // 继续执行过滤器链
}
}
3)Filter与项目登录验证功能集成
1.先要捋顺逻辑,首先判断是否是登录操作,是就放行,不是再进行校验
拦截器Interceptor
1)使用Interceptor
Interceptor使用需要两步,1.要先定义拦截器,通过实现HandlerInterceptor接口来定义(需要用@Component注解声明为Springboot中的组件);2.注册拦截器,通过实现WebMvcConfigurer来实现(需要用@Configuration注解声明为配置类)
2)拦截路径的分类:
拦截路径,如果要拦截多个,方法addPathPatterns和excludePathPatterns,参数类型是可变的参数类型,所以随意写;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**") // 拦截所有路径
.excludePathPatterns("/depts/**", "/emp/**"); // 排除特定路径及其子路径
}
// 其他配置...
}
3)拦截流程:
拦截的流程,Filter是Web应用架构的,所以设置后,无论如何都会经过它,然后请求会来到Spring框架中,Spring框架中定义了一个DispatcherServlet用来分发请求到对应的controller中,中间就夹着一个拦截器Interceptor;
七、统一异常处理
目前,我们的项目出现异常其实都没有处理。例如Controller调用Service,service调用mapper,然后mapper出现问题,就会一层层往上跑,最后controller也往外抛,就会抛给框架底层,底层会打印信息返回给前端。但是和前端要求返回的格式是不一致的,这样就会无法在前端页面展示给用户。
所以,我们需要一个全局的处理器。
1)定义一个全局处理器,需要利用注解:
@RestControllerAdvice = @Controller+@ResponseBody
2)方法上需要使用注解@ExceptionHandler,定义要捕获的注解类型;
八、事务处理
程序上面,还有业务这一层。对于不同的业务,事务管理还是很有必要的,例如,删除部门的时候,其实要删除对应部门下的员工才可以。而这时候如果不是同一个事物,就有可能导致员工删了,部门没删;或者部门删了,员工没删;
九、AOP
所谓的AOP,我理解是代码写了很多了,然后如果统一要对部分类似代码/同包代码进行处理,如果要全部改一遍很麻烦,不好处理。这时候,就产生了AOP的方法,把要处理的逻辑抽象具体为具体的方法,(要实现这种效果,底层是用到了注解、以及动态代理技术的),然后统一对这部分代码进行逻辑增强。
底层实现逻辑:
AOP的好处:
AOP关键知识:
1)使用步骤:
1.引入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2.定义一个类,使用注解@Aspect,@Component,声明这个类为AOP类,归属于springboot管理;
3.按照模板来写具体要抽取的核心逻辑
@Around("execution(* com.itheima.service.*.*(..))")//这是around的写法
//方法参数是固定的,定义为了proceedingJoinPoint
public Object recordTime(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
long begin = System.currentTimeMillis();//其他要在这个方法上执行的逻辑
Object object = proceedingJoinPoint.proceed(); //调用对应匹配到的方法,然后运行
long end = System.currentTimeMillis();
log.info(proceedingJoinPoint.getSignature()+"执行耗时: {}ms", end - begin);
return object;
}
2)AOP的概念:
在AOP中,通知(Advice)表示要在目标方法上执行的额外逻辑,而切面(Aspect)则负责定义通知应该应用到哪些连接点(Join Point)上。连接点是程序执行过程中的一个点,如方法的调用或异常的处理。
核心概念:
1)通知:
记住:只有Around才需要自己调一遍proceed,其他的其实没有必要;
通知顺序
2)切入点表达式
3)连接点
4)切入点
5)案例:将案例中 增、删、改 相关接口的操作日志记录到数据库表中
1.定义一个切面类、定义好日志表实体类、以及mapper(插入数据库表的操作):
注意:还要新建一个自定义注解类
2.在切面类中抓取要记录的日志数据:
// @Around("execution(* com.itheima.service.impl.*.*(..))")
@Around("@annotation(com.itheima.anno.Log)")
//这里使用注解类,是为了方便针对具体的方法使用,而不是放到包这个维度,不好管控
public Object recordTime(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
// proceedingJoinPoint.getThis();
//记录操作人ID - 当前登录员工ID
//获取请求头中的jwt令牌, 解析令牌
String jwt = httpServletRequest.getHeader("token");
Claims claims = JwtUtils.parseJwt(jwt);
Integer operateUser = (Integer) claims.get("id");
//获取操作时间
LocalDateTime operateTime = LocalDateTime.now();
//获取操作的类名
String className = proceedingJoinPoint.getTarget().getClass().getName();
//获取操作的方法名
String methodName = proceedingJoinPoint.getSignature().getName();
//获取方法参数
Object[] Params = proceedingJoinPoint.getArgs();
String methodParams = Arrays.toString(Params);
//获取返回值
String returnValue = proceedingJoinPoint.getKind();
long begin = System.currentTimeMillis();
Object object = proceedingJoinPoint.proceed(); //调用原始方法运行
long end = System.currentTimeMillis();
long costTime = end - begin;
log.info(proceedingJoinPoint.getSignature()+"执行耗时: {}ms", end - begin);
OperateLog operateLog = new OperateLog(operateUser,operateTime,className,methodName,methodParams,returnValue,costTime);
LogMapper.insertLog(operateLog);
return object;
}
3.具体的类中,需要加上@log注解(好像一般都是加在controller的…是因为这是一切的开始吗?)
附录1:Springboot的yml配置
在Springboot项目中,经常要在application.properties中写配置;还有另外一种方式写配置,那就是yml文件。
1)编写yml文件
yml语法:缩进是严格的,层级不正确有可能读取不了配置;
- 大小写敏感数值前边必须有空格,作为分隔符
- 使用缩进表示层级关系,缩进时,不允许使用Tab键,只能用空格(idea中会自动将Tab转换为空格)
- 缩进的空格数目不重要,只要相同层级的元素左侧对齐即可
- #表示注释,从这个字符一直到行尾,都会被解析器忽略
2)写完yml文件后,如果要使用yml文件中的数据,有两种使用方式:
1.利用@Value这个注解,为属性进行单个的赋值:@Value 注解通常用于外部配置的属性注入,具体用法为: @Value(“${配置文件中的key}”)
类必须交给IOC管理;使用Component管理;
2.如果感觉写value,要每一个都写一遍前缀太麻烦,还可以全部提到开头,用一个前缀代替:
附录2:手机访问项目:
如果您已经确认手机和电脑连接的是同一个WiFi网络,并且您尝试通过https://[电脑地址]:90访问项目但无法成功,可能有几个原因:
使用了HTTPS而不是HTTP:如果您的Nginx服务器没有配置SSL证书来支持HTTPS,那么使用https://协议来访问是无法成功的。默认情况下,Nginx不会为90端口配置SSL,除非您明确进行了设置。如果您只是想通过HTTP访问项目,您应该使用http://[电脑地址]:90。
防火墙或安全组策略:电脑的防火墙或路由器的安全策略可能阻止了90端口的外部访问。您需要检查电脑的防火墙设置,确保90端口是开放的。如果您使用的是公司或学校的网络,还可能需要咨询网络管理员。
Nginx配置问题:可能Nginx没有正确配置来监听90端口,或者没有正确设置来提供前端项目的文件。您需要检查Nginx的配置文件,确保server块中的listen指令设置为90,并且root指令指向了正确的项目目录。
网络问题:尽管手机和电脑连接的是同一个WiFi,但有时候网络设置或路由器的问题可能导致它们之间无法正常通信。您可以尝试重启路由器或检查网络的连接情况。
浏览器缓存或DNS问题:有时候,浏览器缓存或DNS缓存可能导致访问问题。您可以尝试清除浏览器缓存或使用其他浏览器尝试访问。
IP地址输入错误:请确保您输入的IP地址是正确的,并且没有输入错误。
为了解决这个问题,您可以按照以下步骤操作:
检查Nginx配置:确保Nginx配置文件中有一个server块监听在90端口,并且项目的根目录设置正确。
重启Nginx服务:在修改Nginx配置文件后,需要重启Nginx服务来使更改生效。
检查防火墙设置:确保电脑的防火墙没有阻止90端口的访问。
尝试使用HTTP:如果您没有配置SSL证书,尝试使用http://[电脑地址]:90来访问项目。
检查网络连接:确保手机和电脑都能正常连接到WiFi网络,并且没有其他网络问题。
如果以上步骤都无法解决问题,您可能需要进一步检查Nginx的错误日志,或者在命令行中使用工具(如curl或telnet)来测试端口是否开放和Nginx是否响应。