目录
完成了前面所有的模块之后,具体可以看我前面的博客,我们今天接下来完成登录校验、全局异常处理与事务管理和AOP
登录模块
简单的登录可以去查询数据库是否有这个员工,可以按照三层架构去完成
Controller层
新建一个文件LoginController,表示专门处理登录
import com.itheima.pojo.Emp;
import com.itheima.pojo.Result;
import com.itheima.service.EmpService;
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.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
public class LoginController {
@Autowired
private EmpService empService;
@PostMapping("/login")
public Result login(@RequestBody Emp emp) {
log.info("员工登录,em={}", emp);
Emp e = empService.login(emp);
return e != null ? Result.success() : Result.error("用户名或密码错误");
}
}
Service层
EmpService
/*
* 登录
* */
Emp login(Emp emp);
EmpServiceImpl
@Override
public Emp login(Emp emp) {
return empMapper.getByUsernameAndPassword(emp);
}
为啥不是login方法名,而是getByUsernameAndPassword 。因为login是业务层的方法,mapper层是持久层直接操作数据库的,所以就该名成getByUsernameAndPassword
Mapper层
EmpMapper
/*
* 登录
* */
@Select("select * from emp where username = #{username} and password = #{password}")
Emp getByUsernameAndPassword(Emp emp);
这只是一个简单sql语句,所以直接写在注解上
那么运行程序,先点击退出
然后输入用户名和密码,
注意:用户名和密码要在数据库里面有才行!
点击登陆之后, 成功回到这个页面说明登陆功能完成
登录校验JWT
以上登录是不是少了什么?所以我们要进行 登录校验
为啥要用JWT令牌,因为对比了cookie和session,JWT优势更大,弊端更小
1.那么首先在pom.xml文件引入依赖
<!--引入servelet依赖--> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> </dependency> <!--JWT令牌--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <!--fastJSON--> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.76</version> </dependency>
2.然后我们在utils这个包当中,引入工具类
JwtUtils
package com.itheima.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.Map;
public class JwtUtils {
private static String signKey = "itheima"; // 签名算法指定的Key值
private static Long expire = 86400000L; // 有效时间长度
/**
* 生成JWT令牌
* @param claims JWT第二部分负载 payload 中存储的内容
* @return
*/
public static String generateJwt(Map<String, Object> claims){
String jwt = Jwts.builder()
.addClaims(claims)
.signWith(SignatureAlgorithm.HS256, signKey)
.setExpiration(new Date(System.currentTimeMillis() + expire))
.compact();
return jwt;
}
/**
* 解析JWT令牌
* @param jwt JWT令牌
* @return JWT第二部分负载 payload 中存储的内容
*/
public static Claims parseJWT(String jwt){
Claims claims = Jwts.parser()
.setSigningKey(signKey)
.parseClaimsJws(jwt)
.getBody();
return claims;
}
}
3.不用更改工具类代码,之后在原来的LoginController文件,代码改成如下
@Slf4j
@RestController
public class LoginController {
@Autowired
private EmpService empService;
@PostMapping("/login")
public Result login(@RequestBody Emp emp){
log.info("员工登录,em={}",emp);
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("useranme",e.getUsername());
String jwt = JwtUtils.generateJwt(claims);
return Result.success(jwt);
}
//登录失败返回登陆失败信息
return Result.error("用户名或者密码错误");
}
}
那么启动程序,然后编写一个测试,这是一个POST请求,输入用户名和密码json传入参数
然后点击发送,data里面就是返回来的token值
然后查询部门接口这个测试一下,但此时如果我们不携带上token这个请求头参数,是不会成功的,所以我们需要在Headers添加token这个参数,把刚才返回来的token值复制过去,然后点击发送,
如果成功返回结果,说明没问题
然后在前端进行联调,如果登录成功那就是没问题的。
部门管理数据正常显示,确实没问题,嘿嘿~~
拦截器
拦截器就是在发起登陆之后执行controller之前,把请求拦截一下,校验登录是否合法,所以我们要在preHandle编写
那么我们新建一个interceptor包,并在该包下新建一个LoginCheckInterceptor登录拦截器
如下
代码如下
package com.itheima.interceptor;
import com.alibaba.fastjson.JSONObject;
import com.itheima.pojo.Result;
import com.itheima.utils.JwtUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.stereotype.Component;
import jakarta.servlet.http.HttpServletRequest; // 修改为jakarta
import jakarta.servlet.http.HttpServletResponse; // 修改为jakarta
import org.springframework.web.servlet.ModelAndView;
@Slf4j
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
@Override // 目标资源方法运行之前执行,返回true,放行;返回false不放行
public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception {
System.out.println("preHandle... 登录校验");
//1.获取请求的url
String url = req.getRequestURL().toString();
log.info("请求的url={}", url);
//2.判断请求的url是否包含login,是的话则是登录操作,放行
// 由于不拦截/login的请求,所以这段代码可以直接删掉
if (url.contains("login")) {
log.info("是登陆状态,放行");
return true; //拦截器的放行就是return true
}
//3.获取请求头中的令牌(token)
String jwt = req.getHeader("token");
//4.判断token是否存在,不存在返回错误的信息
if (!StringUtils.hasLength(jwt)) {
log.info("请求头token为空,返回错误信息");
Result error = Result.error("NOT_LOGIN");
//手动转换 将对象-->转换成JSON格式的字符串
String notLogin = JSONObject.toJSONString(error);
resp.getWriter().write(notLogin);
return false;
}
//5.解析token,如果报错 ,返回错误信息
try {
JwtUtils.parseJWT(jwt);
} catch (Exception e) {
e.printStackTrace();
log.info("jwt令牌解析失败,返回错误登录信息");
Result error = Result.error("NOT_LOGIN");
//手动转换 将对象-->转换成JSON格式的字符串
String notLogin = JSONObject.toJSONString(error);
resp.getWriter().write(notLogin);
return false;
}
//6.放行
log.info("令牌解析成功,放行");
return true;
}
}
然后新建一个config包,用来配置拦截器应该拦截哪些路径
如下,创建一个WebConfig类
代码如下,表示拦截所有路径除了/login
package com.itheima.config;
import com.itheima.interceptor.LoginCheckInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration //配置类
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginCheckInterceptor loginCheckInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**").excludePathPatterns("/login");
}
}
然后你退出登录,再次登录进去,如果能正常登录并且数据正常显示,那么拦截器配置成功
至此登录校验功能全部完成
全局异常处理
如果在那些controller层出现了错误,那么是不是要加上try。。。catch。。。捕获异常并处理啊,但是如果每个地方都要加上try。。。catch。。。是不是会太多了?并且代码看起来繁琐,所以我们可以用全局异常处理器去处理异常,来捕获所有异常做统一的处理。
那么我们创建一个exception包,在里面新建一个GlobalExceptionHandler表示全局处理器
然后编写如下函数
import com.itheima.pojo.Result;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/*
* 全局异常处理器
* */
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class) //捕获所有异常
public Result ex(Exception ex){
ex.printStackTrace();
return Result.error("对不起操作失败,请联系系统管理员~");
}
}
@ExceptionHandler:用于指定异常处理方法。当与@RestControllerAdvice配合使用时,用于全局处理控制器里的异常。Exception.class表示捕捉到所有异常,@RestControllerAdvice=@ControllerService+@RequestBody
捕捉到异常之后,报错,然后返回给前端错误
那么我们在部门管理故意制造一个异常,如下图,制造一个除以0的异常
那么你就可以看到这个弹窗Toast,说明你的全局异常处理器是生效的。
事务管理
首先事务是什么我们得了解一下
那么我们回想一下,我们删除部门的同时是不是也得把对应的部门id删除掉,所以可以把这两个操作全部放在部门删除这个功能里,并添加上事务即可。部门以及对应部门id的员工删除要么同时成功,要么同时失败
所以我们在业务层(为啥是业务层,因为具体的操作逻辑是在业务层编写的)的DeptServiceImpl中的删除员工部分的代码加上注解,并标记rollback回滚所有异常,
记得在前面注入EmpMapper对象
然后在EmpMapper编写根据部门id删除员工代码
/*
* 根据部门id删除员工
* */
@Delete("delete from emp where dept_id = #{deptId}")
void deleteByDeptId(Integer deptId);
然后将我们在controller层,人为制造的异常代码删掉
之后运行程序,如果删除没问题,那么删除部门id同时也会把对应部门的员工删除没问题了
咨询部删除了,并且删除成功
同时,属于咨询部的员工的数据也没了,说明删除成功。
AOP
AOP是什么,就是它可以统一进行事务管理,记录操作日志等等一系列的繁琐操作。
所以接下来我们来用AOP去实现记录日志操作的案例
首先我们导入AOP的依赖,然后刷新maven图标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
然后我们新建一个anno包,然后定义一个MyLog的Annotation
去自定义MyLog注解
package com.itheima.anno;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
//创建自定义的注解
//annotation就是注解的意思
@Retention(RetentionPolicy.RUNTIME) //@Retention 什么时候生效,RetentionPolicy.RUNTIME运行时生效
@Target(ElementType.METHOD) //@Target 可以作用在哪些地方,ElementType.METHOD 作用在方法上
public @interface MyLog {
}
之后在pojo实体类导入对象OperateLog,这是代表日志操作的对象
package com.itheima.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class OperateLog {
private Integer id; //ID
private Integer operateUser; //操作人ID
private LocalDateTime operateTime; //操作时间
private String className; //操作类名
private String methodName; //操作方法名
private String methodParams; //操作方法参数
private String returnValue; //操作方法返回值
private Long costTime; //操作耗时
}
之后记得在数据库新建一个表名为operate_log,sql语句如下
create table tlias.operate_log
(
id int unsigned auto_increment comment 'ID'
primary key,
operate_user int unsigned null comment '操作人ID',
operate_time datetime null comment '操作时间',
class_name varchar(100) null comment '操作的类名',
method_name varchar(100) null comment '操作的方法名',
method_params varchar(1000) null comment '方法参数',
return_value varchar(2000) null comment '返回值',
cost_time bigint null comment '方法执行耗时, 单位:ms'
)
comment '操作日志表';
之后在mapper层,新建一个OperateMapper,表示他会把得到的数据插入到数据库表中
package com.itheima.mapper;
import com.itheima.pojo.OperateLog;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface OperateLogMapper {
//插入日志数据
@Insert("insert into operate_log (operate_user, operate_time, class_name, method_name, method_params, return_value, cost_time) " +
"values (#{operateUser}, #{operateTime}, #{className}, #{methodName}, #{methodParams}, #{returnValue}, #{costTime});")
public void insert(OperateLog log);
}
然后新建一个aop包,然后定义一个AOP操作 ,用来获取操作人ID,操作时间等等
LoaAspect的代码,注解@Aspect就代表AOP类
package com.itheima.aop;
import com.alibaba.fastjson.JSONObject;
import com.itheima.mapper.OperateLogMapper;
import com.itheima.pojo.OperateLog;
import com.itheima.utils.JwtUtils;
import io.jsonwebtoken.Claims;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.Arrays;
@Slf4j
@Component
@Aspect //AOP类
public class LogAspect {
@Autowired
private HttpServletRequest request;
@Autowired
private OperateLogMapper operateLogMapper;
@Around("@annotation(com.itheima.anno.Log)")
public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable {
//操作人ID -->可以通过jwt来获取
String jwt = request.getHeader("token");
Claims claims = JwtUtils.parseJWT(jwt);
Integer OperatorUserId = (Integer) claims.get("id");
//操作时间
LocalDateTime operateTime = LocalDateTime.now();
//操作类名
String className = joinPoint.getTarget().getClass().getName();
//操作方法名
String methodName = joinPoint.getSignature().getName();
//操作方法参数
Object[] argsName = joinPoint.getArgs();
String methodParams = Arrays.toString(argsName);
//操作方法返回值
//执行原始方法,获取返回值(环绕通知)
long begin = System.currentTimeMillis();
Object result = joinPoint.proceed();
String returnValue = JSONObject.toJSONString(result);
long end = System.currentTimeMillis();
//操作耗时
Long costTime = end- begin;
//记录i操作日志
OperateLog operateLog = new OperateLog(null,OperatorUserId,operateTime,className,methodName,methodParams,returnValue,costTime);
operateLogMapper.insert(operateLog);
log.info("插入操作的日志{}",operateLog);
return result;
}
}
最终我们在service层的实现类加上我们自定义的注解,比如要DeptServieImpl中,在插入、查询全部部门数据的方法上加上@MyLog注解
那么当我们程序启动,并执行插入部门操作的时候,如果出现如下图
能在控制台看到这条语句
并且在数据库中查看到最新的操作
则表示AOP管理我们已经编写完成了。
那么至此我们已经把所有后端的功能全部圆满完成了。JavaWeb的学习就告一段落了,之后希望我的6篇javaweb的学习博客能给大家带来帮助,有不懂的可以在评论区提问噢,我会热心解答!也欢迎大家提出不足的地方!之后我会不定期的更新高质量博客,期待大家的关注!