一)SpringAOP的实现原理:
Spring的切面由包裹了目标对象的代理类实现,代理类处理方法的调用,执行额外的切面逻辑,调用目标方法,使用动态代理技术实现方法的调用
首先要有一个目标对象,在上面的例子中,目标对象就是UserController,然后通过这个目标对象,创建一个代理类,并且在某一个规定的时机生成,这个时机就是织入,最后在这个代理类上面添加一些增强的方法,这个过程就叫做植入;
1)目标对象:代理的目标对象
2)织入:就是代理的生成时机,织入是将将切面应用到目标对象并创建新的代理对象的过程,切面在指定的植入点被植入到目标对象中,在目标对象的生命周期中可以有多个点被植入
a)编译期间:切面在编译器被植入,这种方式需要特殊的编译器,AspectJ就是通过这种方式来植入切面的
b)类加载器:切面在目标类加载到JVM的时候被植入,这种方式需要特殊的类加载器
c)运行期间:切面在应用运行的某一段时间就被植入,一般情况下,在织入切面的时候,AOP会为目标对象动态的生成一个代理对象,SpringAOP就是以这种方式来织入切面的
在不修改代码的前提下,引入可以在运行期为类动态地添加一些方法或字段
织入:代理的生成时机,代理在什么时期生成的,也是输入AOP的一个定义,和切面切点,连接点是一样的,比如说咱们的lombok,就是在编译时期进行搞事情的,生成Getter和Setter方法;
织入:AOP术语,把切面aspect连接到其它的应用程序类型或者对象上,并创建一个被通知advised的对象,这样的行为叫做织入;
织入的意思就是把切面应用到目标对象并创建新的代理对象的过程,切面在指定的连接点被植入到目标对象中,在目标对象的生命周期里面有多个点可以被植入;
编译期:切面在目标类编译时候进行注入,这种方式需要特殊的编译器,AspectJ和LomBok就是在编译时期搞事情的,Lombok就是把在编译的时候把注解生成对应的代码;
类加载期:切面在目标类里面加载到JVM的时候进行织入,比如说饿汉模式虽然类加载时间比较长,但是用的时候就会很快,但是如果准备好了不用,就会发现白类加载了;
运行期:切面在应用运行的某一个时刻被织入,一般情况下,在进行织入切面的时候,AOP容器会为目标对象动态创建一个代理对象,SpringAOP就是以这种方式进行植入的,是在运行期植入,class代码运行阶段,采用的是懒汉模式,启动的时候不会加载,直到用的时候才类加载;
二)SpringAOP统一功能处理
接下来要介绍的是SpringBoot同一功能处理模块了,也就是AOP的实战环节,这种方法SpringAOP原生的用法又是不太一样了
1)统一的用户登陆权限验证
2)统一的数据格式返回,之前实现的方式是我们直接创建一个公共对象,每一次方法返回时候都new一个公共对象,返回的数据格式都是统一的,但是问题是我们每一次都是new对象,创建对象,开发效率比较低,比较麻烦,耦合性太强;
3)统一的异常处理,后端服务器出现错误,出现了500,当前端传送AJAX,出现异常统一封装成JSON格式数据,否则ajax里面的function不会被调用到,前端本来是想要JSON格式的数据,但是后端却返回了一个报错信息,根本就不会给success方法;
Spring针对原始的AOP又进行了封装,是基于AOP,让我们的写法变得更加简单了;
Spring拦截器:注册功能和注册功能是不可以进行拦截的,以及访问一些静态页面;
不通过原始的SpringAOP的切面方法来进行实现用户登录校验的功能是有下面两个原因:
1)我们是没有办法设置一个HttpSession对象的,之前写的前置通知,后置通知都是不可以的
2)切点是有拦截规则的,我们要对一部分方法进行拦截,而另一部分方法是不会进行拦截的,比如说咱们的登录功能和注册功能是不会进行拦截的,这样的话我们进行排除方法的规则是很难进行定义的,甚至都没有办法进行定义,甚至前端的CSS,HTML都是不能拦截的,所以说使用@AspectJ语法还是不行的;
一:先进行创建自定义拦截器:HandlerIntercepter
拦截规则可以拦截此项目中的使用的url,包括静态文件,图片文件,JS和CSS文件,除了登录和注册功能不拦截以外,其他均需要拦截url;
1)进行创建自定义拦截器,这个拦截器是一个普通的拦截器,创建一个类实现HandlerIntercepter接口重写preHandle,执行具体方法前的预处理方法,这个拦截器里面的preHandle的方法返回值是boolean类型,在这个方法体里面咱们是可以进行登录功能的验证的,如果通过了验证业务那么直接返回true,可以访问后端的接口,如果说没有通过验证,那么直接返回false,直接打回前端
2)这个preHandle方法中的参数有HttpServletRequest和HttpServletResponse参数
如果说咱们的程序执行的返回值是true,那么说明这个拦截器通过了,就可以放行了访问后面的方法,但是如果返回false,通过Response对象返回一个状态码,或者是直接通过Response对象进行设置重定向,这个拦截器是不用加类注解的,直接打回给前端
此时实现的拦截器是一个普通的拦截器,而不是一个Spring的拦截器,换句话来说,并不会在咱们的SpringBoot项目启动之后进行加载
二:将自定义拦截器加入到SpringBoot项目里面,就是把这个拦截器加入到WebMvcConfigurer中的addInterceptors方法参数里面,将自定义拦截器加入到系统的配置的框架中,并设置拦截的规则
1)创建一个类,实现WebMvcConfigure接口,类上面再加上@Configuration注解
2)在类里面重写addInterceptors方法,把这个类注册到SpringBoot里面
3)在addInterceptors方法里面有一个参数,叫做InterceptorRegistry registry,我们调用里面的
3.1)addInterceptor(new 拦截器对象)方法,表示把我们的拦截器对象注入到SpringBoot项目里面
3.2)addPathPatterns(拦截的url路径):表示拦截的URL,执行拦截器的时候,指定进行拦截的URL
3.3)这里面的"**"表示拦截任意方法,也就是所有方法。
3.4)excludePathPatterns(排除的url路径):执行拦截器的时候,表示需要进行排除的URL
只有我们的拦截器的返回值是true的时候,才会继续执行Controller里面的方法
3.5)由此可知,一个SpringBoot项目可以配置多个拦截器;
- 1)我们现在一个包里面创建一个类,先进行自定义一个拦截器,是一个普通的类,重写方法
- //这个类没有加注解,不会注册到SpringBoot里面,这时候是不会生效的,况且这个拦截器还没有实现拦截的规则,没有这方面的配置
- public class LoginSession implements HandlerInterceptor {
- @Override
- public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception {
- HttpSession session= req.getSession(false);
- if(session==null||session.equals(""))
- {
- return false;
- }
- UserDemo userDemo=(UserDemo)session.getAttribute("UserDemo");
- if(userDemo==null||userDemo.equals(""))
- {
- return false;
- }
- //在这里面还可以进行重定向到一个页面
- return true;
- }
- }
SpringMVC 拦截器拦截 /* 和 /** 的区别: /* : 匹配一级,即 /add , /query 等 /** : 匹配多级,即 /add , /add/user, /add/user/user… 等
- 2.我们在从上面的同一个包在进行创建一个类,用组件修饰
- 将刚才的拦截器注册到这个类里面
- 在这个类进行注册拦截器和配置拦截规则:
-
- @Configuration//声明这个类是系统的一个配置类
- public class SpringInsert implements WebMvcConfigurer {
- @Override
- public void addInterceptors(InterceptorRegistry registry) {
- registry.addInterceptor(new LoginSession())
- .addPathPatterns("/**")//表示拦截所有请求,现在访问所有请求都是空页面
- .excludePathPatterns("/**/*.html")
- //但是此时访问静态页面,所有的图片和CSS样式都是加载不出来的
- //加上/**之后可以进行拦截多级目录
- .excludePathPatterns("/**/*.css")//排除接口
- .excludePathPatterns("/**/*.js")
- .excludePathPatterns("/**/*.png")
- .excludePathPatterns("/**/login");
- 第一层路径不管是啥,最后的文件名是啥也不管
- //除此之外,登录和注册功能是不需要进行拦截的
- registry.addInterceptor(new 新的拦截器)
-
- }
- }
在这里面还是要注意一个问题:咱们的/**/*.html和/*.html有什么区别呢?
如果说咱们的url的访问路径是127.0.0.1:8080/Java100/blog1.html,这个时候我们的路径/*.html是无法进行生效的,因为它只能匹配一级目录,而咱们的第一种写法是可以进行匹配N级目录的;
拦截器的实现原理:动态代理和环绕通知的思想
所有前端的请求进来之后会先进入到调度器进行分发,也就是DispatcherServlet
1)就比如说送货,送货人直接把一堆货品送到各类人所在城市的公共营业点,不是说发送方必须知道每一个货品所在的接收人的地址,给你分别送到家,这样的分发有站点比较方便
2)然后营业点就会把这些货品进行分类,分发到各个小区的快递站,每一个人处理的线路都是单一的
1)宏观角度:是基于AOP的思想来进行设置的,把统一的问题放到前置处理器进行处理了,就是在调度器对象DispatcherServlet中的doDispatch方法里面的有一个扫描拦截器的步骤,如果拦截器拦截通过,继续执行Controller里面的代码,如果拦截不通过直接return,就是一个空页面
2)在真正调用Controller修饰的方法之前,先去扫描Spring所有拦截器的列表,只有当这个方法的所有的拦截器都通过之后,返回true,才会继续走正常的流程,才会执行被Controller修饰的方法,才会给返回结果给前端;
3)就是通过动态代理和环绕通知的思想来进行实现的,是调度器,代理对象来进行决定来调用我们的哪一个Controller;
统一访问前缀增加:
1)定义一个类,实现WebMvcConfigurer接口,实现addInterceptors方法是用来定义拦截规则和存放拦截器的
2)现在我们在从这个类里面重写configurePathMatch方法,进行添加前缀
3)我们还要注意一下:咱们的这个前缀的增加,是访问路径多了一级:
比如说咱们之前的访问路径是127.0.0.1:8080/User/Reg,咱们执行了添加前缀方法之后,咱们的访问路径就变成了127.0.0.1:8080/API/User/Reg才可以进行正常的访问
但是我们还是要注意一个问题呀,我们来看下面的一个流程,所以我们一定要加上/**
1)我们想要进行登录验证的操作,我们是想要排除进行注册的方法的,就会调用excludePathPatterns("/User/Reg"),我们的注册功能肯定是不会进行拦截的
2)但是我们执行了增加前缀的功能之后,我们还是用上面的excludePathPatterns字符串之后,我们此时再用127.0.0.1:8080/API/User/Reg,此时我们啥也访问不到,因为就是说我们没有进行设置要刨除/API/User/Reg的方法,上面的那种写法只是只对一级路径管用,所以咱们应该写成:excludePathPatterns("/**/User/Reg"),此时我们在从浏览器上进行访问,才可以正常的执行我们的业务流程
-
- @Configuration
- public class SpringInsert implements WebMvcConfigurer {
- @Override
- public void addInterceptors(InterceptorRegistry registry) {
- registry.addInterceptor(new LoginSession())
- .addPathPatterns("/**")//表示拦截所有请求,现在访问所有请求都是空页面
- // .excludePathPatterns("/**/*.html")//但是此时访问静态页面,所有的图片和CSS样式都是加载不出来的,加上/**之后可以进行拦截多级目录
- // .excludePathPatterns("/**/*.css")
- // .excludePathPatterns("/**/*.js")
- // .excludePathPatterns("/**/*.png")
- .excludePathPatterns("/**/GetAll");
- }
-
- @Override//设置API统一的访问前缀
- public void configurePathMatch(PathMatchConfigurer configurer) {
- configurer.addPathPrefix("API",c->true);
- //c是一个随便的名字,是针对我们所有的Controller接口都加上一个前缀,加上一级目录
- }
- }
也可以通过属性注入的方式来注入拦截器对象
@Service public class Hander implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession httpSession=request.getSession(false); if(httpSession==null||httpSession.getAttribute("User")==null) { response.setContentType("text/plain;charset=utf-8"); response.getWriter().write("您当前没有进行登录"); //或者直接请求重定向到一个页面 resp.sendRediect(地址); return false; } return true; } }
@Configuration class SpringInset implements WebMvcConfigurer { @Autowired private Hander hander; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor( hander) .addPathPatterns("/**") .excludePathPatterns("/**/**.html") .excludePathPatterns("/**/**.css") .excludePathPatterns("login.html") .excludePathPatterns("register.html") .excludePathPatterns("/**/**.js") .excludePathPatterns("/**/**.png"); } }有了拦截器以后,会在调用Controller之前执行相应的业务处理
没有拦截器之前:
现在有拦截器之后:
1)所有的Controller的执行都是通过在通过一个调度器DispatcherServlet来实现,随便访问Controller中的一个方法就可以在控制台的打印信息就可以看到,这个可以类比于线程调度上
2)然而所有的Controller的方法都会执行DispatcherServlet中的调度方法doDispatch(),源码的关键步骤就是预处理的过程,就和上面的LoginInterceptor做的事情是一样的,判断拦截的方法是否符合需求,如果符合要求就返回true,然后继续执行后续业务代码,否则后面的代码就都不执行;
3)下面是DispatcherServlet的doDispatch()的源码
3.1)applyPreHandler执行的是拦截器的方法,如果返回的是false,这里面就直接返回了,后续的目标方法就不会执行
3.2)isConcurrentHandlingStarted()返回的是true,就是开始执行Controller中的方法
if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; } mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); if (asyncManager.isConcurrentHandlingStarted()) { return; }4)applyPreHandler源码分析:
再进行调用Controller方法之前,DispatchServlet会按照顺序调用所有拦截器的preHandler方法,如果所有的preHandler方法返回的都是true,那么处理流程继续,否则停止
其实源码中就是进行遍历存放拦截器的List,然后不断地进行判断每一个拦截器是否都返回true了,但凡有一个拦截器返回的是false,后面的拦截器都不要走了,并且后续的业务代码也不会执行了,通过上述内容就可以看出Spring中的拦截器其实就是利用了传统思想的AOP,它是通过动态代理和环绕通知的思想来实现的
三)实现对统一的异常处理:
@ControllerAdvice:定义一个全局异常处理类,用于在进行处理在Controller层中抛出的各种异常,并且针对于异常来做统一的处理,使用@ControllerAdvice注解可以将异常处理逻辑从Controller中解耦合,提高代码的复用性;
@ExceptionHandler:定义异常处理方法,使用@ExceptionHandler,可以根据不同的异常类型进行处理;
1)正常后端如果出现了500的错误,后端就会直接给前端返回一个text/html,也就是500的一个异常信息
2)给所有的方法如果加上事务,一方面是代码不好管理,另一方面学习事务加上try catch是不可以自动回滚事务的
1)我们进行创建一个类,在这个类上面加上@ControllerAdvice注解,它的作用是告诉Spring当前类是一个增强类,它增强了对异常的统一管理,是一个控制器通知类,表明会随着SpringBoot项目的启动而添加到Spring里面,咱们的统一异常处理的方法具体的业务代码就会被写到这个类里面;
2)我们在这个类里面创建一个异常处理方法,在这个方法里面加上注解@ExceptionHander(Exception.class),里面可以写咱们的要发生的异常类型,表示如果发生了一个Exception的异常,那么会执行到到这个方法里面的代码,会把封装的结果返回到前端,这个注解是进行修饰方法的,在这个方法里面,我们就可以进行添加具体的异常业务处理返回代码,这样这个方法就是异常统一处理方法;
3)你要是给所有的方法都加try catch那是不能进行回滚的,运行时异常我们没有办法感知;
4)@ControllerAdvice就是对于项目里面的事件做拦截的,是派生于Component注解的
- 1)我们之前在代码里面写了前缀叫做API
- 2)我们要防止我们进行访问的接口被拦截器拦截 .excludePathPatterns("/**/HH");
- 3)我们所写的异常统一功能处理:
- @ControllerAdvice
- //加上此注解表示当前是Controller的增强类,也是通知类
- public class ExceptionClass{
- @ExceptionHandler(NullPointerException.class)
- //还可以写自定义异常,当前只能拦截空指针异常
- @ResponseBody
- public HashMap
start(NullPointerException e) - //这里面还可以写成Exception
- {
- HashMap
map=new HashMap<>(); - map.put("sendsuccess",200);
- //请求的接口成功了,但是请求的业务数据报错了,表示HTTP请求成功了
- //这表示请求我们的后端接口成功了
- 但是进行业务处理的时候失败,大状态没有问题
- map.put("state",-1);//表示业务数据处理出错了,小状态出现问题
- map.put("message",""+e.ToString+"后端处理数据失败");
- //返回异常信息
- return map;
- }
- }
直接在浏览器上面进行输入:http://localhost:8080/API/HH,因为后端出现了空指针异常,就直接可以在前端看到报错信息了,这还是一个Json格式的数据
{"sendsuccess":200,"state":-1,"message":"java.lang.NullPointerException"}
1)但是如果说我们没有进行写统一的异常处理代码,就会出现空页面,前端会很蒙的
@RestController 是@controller和@ResponseBody 的结合,最好是出现异常和数据处理的得当的情况下都会返回一个HashMap
2)在上面只写了一个空指针异常的处理方式,但是我们如果在程序中发生了其他异常,我们就捕获不到了,所以说我们还是应该在写一个处理方式,专门处理Exception异常;
四)统一数据返回格式:所有方法的URL都可以进行统一数据的封装
就是缺少灵活性
提前约定好正常执行成功的和输出处理出错返回异常信息都是返回同一种数据类型HashMap
body:返回的数据 state:-1/1 success:200(访问后端接口)1)假设咱们的A方法是查询文章详情的,那么咱们的body数据就是具体的文章的信息,比如说文章内容,发布时间,body就是一个blog对象;
2)但是如果说咱们的B方法是进行查询用户的,那么咱们的body数据就是一个User对象
3)但是说如果我们的程序发生了报错信息,就比如说出现了异常,那么我们的body数据里面就是我们的出错信息
我们进行统一数据返回的数据的好处:
1)方便前端程序员更好地接受和解析后端数据接口返回的数据;
2)降低前端程序员和后端程序员的沟通成本,按照某一个格式实现就可以了,所有的接口都是这样返回的;
3)我们有利于项目统一数据的维护和修改;
4)有利于后端技术的部门的统一规范的标准制定,不会出现稀奇古怪的返回内容;
1)我们要创建一个类,加上@ControllerAdvice注解,我们并让这个类实现ResponseBodyAdvice这个接口
2)重写beforebodyWrite方法和supports方法,我们在supports方法里面将返回值设置成true,并在beforebodyWrite方法里面写我们的业务逻辑,写我们的返回值,实现统一的数据格式处理;
3)如果我们的supports方法的返回值是true,那么执行Controller里面的代码进行返回的业务数据都会经过beforebodyWrite这个方法,并成为Object body这样的参数
4)我们的body数据中有两个值,一个是业务数据,一个是状态,甚至这个业务数据还可以是HashMap;
- @ControllerAdvice
- public class UserAdvice implements ResponseBodyAdvice {
- @Override
- public boolean supports(MethodParameter returnType, Class converterType) {
- return true;
- 返回的是false不表示可以对整个返回结果进行统一,直接返回源数据
- 返回的是true表示可以对整个结果进行封装,表示会进入到beforeBodyWrite方法对数据进行统一的处理
- }
-
- @Override
- public Object beforeBodyWrite(Object body//返回数据的原内容, MethodParameter
- //原始返回数据类型returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
- //统一数据格式封装
- HashMap
hashMap=new HashMap<>(); - hashMap.put("success",200);//这表示请求我们的后端接口成功了,成功的发送了一个HHTTP请求
- 但是进行业务处理的时候失败,大状态没有问题
- hashMap.put("state",1);//表示业务处理请求成功
- hashMap.put("data",body);//这里面的body是我进行访问URL的过程中服务器返回的数据,此后我们每一次进行访问一次URL返回的数据格式都是类似于上面的
- return hashMap;
- }
- }
当我们在浏览器上面输入: http://localhost:8080/API/GetAll
返回的数据:
{"data":[{"classID":1,"classname":"Java","user":{"userID":1,"classID":1,"username":"B","password":"23456"}}],"success":200,"state":1}
简历上面写:
说出项目亮点:
1)密码是用密码加密,保证安全;
2)对所有异常进行了统一的处理,后端代码出现了异常,前端代码是可以感知到的;
3)拦截器进行登录权限校验,是程序更加稳定的运行;
4)@ControllerAdvice是一个感知事件,代码发生异常出现500可以感知,相当于是有了一个通知,自己可以根据感知得到的时间的处理方式写自己的业务代码,相当于是对项目里面的事件做拦截;
5)数据格式返回可以感知,对项目里面的事件做拦截,这个注解也是派生于@Component;
6)加载组件的时候就会判断这些组件是否是拦截事件,当发生了这些事件,程序就可以做出处理;