一.SpringAop
1.SpringAop是一种思想,指的是对使用比较多的功能进行统一处理,比如我们在写博客系统项目,当我们在登录博客列表页和博客详情页以及博客编辑页的时候的时候,都需要写代码进行登录验证,这时候代码就比较繁琐,即使我们将验证是否登录的方法实现好了,那么也需要调用,这时候我们使用AOP的思想,单独用一个地方来进行用户登录验证,每次跳转到博客列表页或者博客详情页或者博客编辑页的时候先去判断是否登录,如果没有登录直接返回,这时候就不需要在当前代码中单独再写方法来判断用户是否登陆了。
2.除了统一的AOP判断之外,AOP还可以实现
(1)统一的日志思想
(2)统一方法执行时间统计
(3)统一的返回格式设置
(4)统一的异常处理
(5)事务的开启和提交
3.AOP的组成
(1)切面:指的是要处理的某一集中功能的整个过程,比如用户登录验证的这个过程
(2)切点:定义了方法和拦截规则,规定了哪些方法需要被拦截
(3)连接点:每一个能触发切点的方法,比如在博客详情页、博客编辑页、博客列表页,都需要判断用户登录状态,这时候就会触发用户登录验证这个切点,博客详情页、编辑页、列表页的登录状态就是连接点
(4)通知:拦截方法的具体要实现的业务
(1)前置通知在目标方法执行之前执行的通知
(2) 后置通知:在目标方法执行之后执行的通知
(3 ) 返回通知:在目标方法返回之后执行的通知
(4)异常通知:在目标方法抛出异常之后通知
(5)环绕通知:在目标方法执行之前和执行之后进行通知
4.SpringAop的实现:
(1)添加SpringAOP的框架支持
(2)定义切面的切点
(3)定义通知
(1)添加AOP框架支持 注意这里是spring-boot的AOP不是spring的AOP
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
(2)定义切面和切点以及相关通知
@Aspect // 切面
@Component // 不能省略
public class UserAop {
// 切点(配置拦截规则)
@Pointcut("execution(* com.example.demo.controller.UserController.*(..))")
public void pointcut() {
}
// 前置通知
@Before("pointcut()")
public void doBefore() {
System.out.println("执行了前置通知:" + LocalDateTime.now());
}
// 后置通知
@After("pointcut()")
public void doAfter() {
System.out.println("执行了后置通知:" + LocalDateTime.now());
}
// 环绕通知
@Around("pointcut()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("开始执行环绕通知了");
Object obj = joinPoint.proceed();
System.out.println("结束环绕通知了");
return obj;
}
}
这里要注意@Aspect这个注解表明这个类是切面类,而@Component这个注解也不能少,只有这样我们AOP框架才会随着我们的spring项目启动而启动,如果不加这个注解,aop没法使用
切点表达式由切点函数组成,其中execution()是最常用的切点函数:execution(<修饰符><返回类型><包.类.方法(参数)<异常>
其中修饰符可以省略,异常可以省略,其他的则不能省略
(3)在UserController底下建立一个类,类里面包含两个方法
@RestController
public class Usercontroller {
@RequestMapping("/user/sayhi")
public String sayHi() {
System.out.println("执行了 sayHi 方法");
return "hi,spring boot aop.";
}
@RequestMapping("/user/login")
public String login() {
System.out.println("执行了 login 方法");
return "do user login";
}
}
当我们起去访问login方法时,login方法会被拦截
当我们下面我们来运行一下看一下运行结果
我们不难发现开始执行环绕通知和前置通知都是在目标方法 执行之前通知,而后置通知和结束环绕通知都是在目标方法执行之后通知,开始执行环绕通知在前置通知前面执行,结束环绕通知在执行后置通知之后执行。
5.SpringAOP的实现原理:
(1)JDK 动态代理:通过反射实现动态代理,其特点速度快
(2)CGLIB:通过字节码增强技术,生成代理类的子类实现动态代理,它不能代理被final修饰类
6.SpringAOP进行用户统一登录验证
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class UserAspect {
// 定义切点⽅法 controller 包下、⼦孙包下所有类的所有⽅法
@Pointcut("execution(* com.example.demo.controller..*.*(..))")
public void pointcut(){ }
// 前置⽅法
@Before("pointcut()")
public void doBefore(){
}
// 环绕⽅法
@Around("pointcut()")
public Object doAround(ProceedingJoinPoint joinPoint){
Object obj = null;
System.out.println("Around ⽅法开始执⾏");
try {
// 执⾏拦截⽅法
obj = joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
System.out.println("Around ⽅法结束执⾏");
return obj;
}
}
用这种方法实现存在两个问题:(1)httpseesion对象我们获取不到(2)拦截规则表达式不好定义, 有的方法需要拦截,但是像登录方法和注册方法则不需要拦截
那这样如何解决呢?
二.统一功能及处理
1.spring拦截器
我们使用spring为我们提供的拦截器,可以方便快捷的实现拦截功能
(1)首先自定义一个拦截器,实现HandlerInterceptor接口,重写preHandle方法
public class LoginIntercpter implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession httpSession=request.getSession(false);
//对所有拦截的方法进行验证,如果验证通过则返回继续执行目标方法
// 如果验证失效,则不执行目标方法
if(httpSession!=null&&httpSession.getAttribute("session_userifo")!=null)
{
return true;
}
response.setStatus(401);
return false;
}
}
(2)将自定义拦截器加入到系统配置,自定义一个类,然后继承WebMvcConfigurer这个接口使用addInterceptor()方法将拦截器加入到系统配置,然后定义拦截规则
Controller
public class Myconfig implements WebMvcConfigurer {
@Autowired
public LoginIntercpter loginIntercpter;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginIntercpter)
.addPathPatterns("/**") //拦截所有的url
.excludePathPatterns("/login")//将登录页排除
.excludePathPatterns("/reg")//将注册页排除
.excludePathPatterns("/image/**")//将image底下所有文件排除
;
}
}
addPathPatterns:表示要拦截的URL,"**"表示拦截任一方法
excludePathPatterns:表示需要排除的URL
以上拦截规则可以拦截项目中使用的URL、包括静态文件(图片文件、JS、CSS等文件)
我们定义一些方法来进行验证
@RestController
public class Usercontroller {
@RequestMapping("/login")
public String login()
{
return"执行了login方法";
}
@RequestMapping("/reg")
public String reg()
{
return "执行了reg方法";
}
@RequestMapping("/bloglist")
public String bloglist()
{
return "执行了bloglist方法";
}
}
当我们访问login方法和reg方法时:
但是当我访问需要被拦截的方法bloglist的时候,因为没有进行登录,没有通过登录验证所以bloglist方法不会被执行,被返回401
但是这里还是存在问题,大家想一下,如果被拦截器 拦截的方法没有经过验证,拦截器返回一个状态码401,这对于前端不友好,因为前端不清楚到底是目标方法出错了,还是没有通过拦截器的验证。因此这里我们最好返回一个json格式的字符串
拦截器原理:
拦截器实现原理源码分析:
每个请求都会经过一个调度器 DispatcherServlet ,由它来决定和分配怎样去执行如何去执行,所有⽅法都会执⾏ DispatcherServlet 中的 doDispatch 调度⽅法, doDispatch 调度⽅法源码如下:
所有统一访问前缀添加:所有请求地址加上api前缀,这种做法主要是为了中小型公司为了节约成本,中小型公司一般一个服务器上可能会部署多个程序,如果两个程序名相同,即使它们的端口名相同,但是也不好区分,而在地址前加上api前缀则解决了这一问题
2.统一异常处理:
当我们去访问一个具体的方法的时候,如果我们的代码抛出异常这时候会直接返回一个状态码500,
这就不够具体,我们应该要和前端约定一个数据格式,当后端出现异常的时候,把这个固定的数据格式返回给前端,那如何来解决这一问题呢?
(1)先来定义一个处理空指针异常的类
@ControllerAdvice
@ResponseBody
public class Myexception {
@ExceptionHandler(NullPointerException.class)
public HashMap<String, Object> donullpoint(NullPointerException e) {
HashMap<String, Object> map = new HashMap<>();
map.put("code", -300);
map.put("msg:", "空指针异常" + e.getMessage());
map.put("data", "");
return map;
}
}
(2)再写一个会出现空指针异常的方法
@RequestMapping("/user1")
public String douser()
{
Object object=null;
System.out.println(object.hashCode());
return "hello user";
}
我们来看一下运行结果:
我们知道当前我们所写的类是指针空指针异常处理的 类,那如果出现的异常不是空指针的异常呢?比如下面出现的算术异常,
那这样的话处理空指针异常的方法就不起作用了,这时我们定义一个能处理所有异常的方法
3.统一数据返回格式
为什么要返回统一数据格式:
(1) 降低了程序员之间的沟通成本,所有的接口按照固定的格式进行返回
(2)有利于项目统一数据的维护和修改
(3)方便前端程序员更好地接收和解析后端数据接口返回的数据
(4)有利于后端技术部门统一规范的标准制定,使返回内容格式统一
我们约定返回数据格式为有code(状态码),msg(信息),data(数据)Map结构
但我们在一个方法里返回1的时候,因为其不符合我们约定的固定的返回数据的格式
@RestController
public class Myfordata {
@RequestMapping("/data1")
public Integer dodata1()
{
return 1;
}
}
我们建一个统一处理格式的类使其符合约定的返回数据格式
注意这个类不需要加ResponseBody注解,因为我们这里只是把不符合格式的数据进行处理,最终返回给前端还是要通过被访问的方法进行返回的
当我们去访问dodate1这个方法时
我们可以看到数据被按照约定的格式返回了
当我们去返回一个类型为boolean类型的时候,因为我们建立了一个统一返回数据格式的类,它也会被按照 规定格式返回。
但是这里有一个特殊的的例子:就是我们返回的数据为String类型的时候
@RequestMapping("/data2")
public String data2()
{
return "helloword2";
}
我们来访问一下:
发现不符合预期,这又是什么原因呢?
(1)方法返回的是String
(2) 统一数据返回之前 将String转换成HashMap的格式
(3)最后将HashMap转换成application/json格式的字符串给前端
在第三步的时候会判断Body的类型,如果是String类型,会交给StringHttpMessageConverter进行类型转换,如果使用StringHttpMessageConverter是不能把hashmap转换成json格式的字符串的,如果不是String类型,会交给HttpMessageConverter进行类型转换
应该如何解决呢?主要有两种解决方案
1.在代码里添加逻辑,如果是String类型,那么我们通过objectMappe这个对象的writeValueAsStrin
g方法把它转换成json格式的字符串
if (body instanceof String) {
try {
body= objectMapper.writeValueAsString(map);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
return body;
2.去掉StringHttpMessageConverter
@Configuration
public class MyConfig implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.removeIf(converter -> converter instanceof StringHttpMessageConverter);
}
}