文章目录
本篇将要学习 Spring Boot 统一功能处理模块,这也是 AOP 的实战环节
- 统一用户登录权限的效验实现接口 HandlerInterceptor + WebMvcConfigurer
- 统一异常处理使用注解 @RestControllerAdvice + @ExceptionHandler
- 统一数据格式返回使用注解 @ControllerAdvice 并且实现接口 @ResponseBodyAdvice
用户登录权限的发展完善过程
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/a1")
public Boolean login (HttpServletRequest request) {
// 有 Session 就获取,没有就不创建
HttpSession session = request.getSession(false);
if (session != null && session.getAttribute("userinfo") != null) {
// 说明已经登录,进行业务处理
return true;
} else {
// 未登录
return false;
}
}
@RequestMapping("/a2")
public Boolean login2 (HttpServletRequest request) {
// 有 Session 就获取,没有就不创建
HttpSession session = request.getSession(false);
if (session != null && session.getAttribute("userinfo") != null) {
// 说明已经登录,进行业务处理
return true;
} else {
// 未登录
return false;
}
}
}
这种方式写的代码,每个方法中都有相同的用户登录验证权限,缺点是:
统一用户登录验证,首先想到的实现方法是使用 Spring AOP 前置通知或环绕通知来实现
@Aspect // 当前类是一个切面
@Component
public class UserAspect {
// 定义切点方法 Controller 包下、子孙包下所有类的所有方法
@Pointcut("execution(* com.example.springaop.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 e) {
e.printStackTrace();
}
System.out.println("Around 方法结束执行");
return obj;
}
}
但如果只在以上代码 Spring AOP 的切面中实现用户登录权限效验的功能,有这样两个问题:
针对上面代码 Spring AOP 的问题,Spring 中提供了具体的实现拦截器:HandlerInterceptor,拦截器的实现有两步:
创建自定义拦截器,实现 Spring 中的 HandlerInterceptor 接口中的 preHandle方法
将自定义拦截器加入到框架的配置中,并且设置拦截规则
1) 给当前的类添加 @Configuration 注解
2)实现 WebMvcConfigurer 接口
3)重写 addInterceptors 方法
注意:一个项目中可以同时配置多个拦截器
(1)创建自定义拦截器
/**
* @Description: 自定义用户登录的拦截器
* @Date 2023/2/13 13:06
*/
@Component
public class LoginIntercept implements HandlerInterceptor {
// 返回 true 表示拦截判断通过,可以访问后面的接口
// 返回 false 表示拦截未通过,直接返回结果给前端
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
// 1.得到 HttpSession 对象
HttpSession session = request.getSession(false);
if (session != null && session.getAttribute("userinfo") != null) {
// 表示已经登录
return true;
}
// 执行到此代码表示未登录,未登录就跳转到登录页面
response.sendRedirect("/login.html");
return false;
}
}
(2)将自定义拦截器添加到系统配置中,并设置拦截的规则
说明:拦截规则可以拦截此项⽬中的使⽤ URL,包括静态⽂件(图⽚⽂件、JS 和 CSS 等⽂件)。
/**
* @Description: 将自定义拦截器添加到系统配置中,并设置拦截的规则
* @Date 2023/2/13 13:13
*/
@Configuration
public class AppConfig implements WebMvcConfigurer {
@Resource
private LoginIntercept loginIntercept;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// registry.addInterceptor(new LoginIntercept());//可以直接new 也可以属性注入
registry.addInterceptor(loginIntercept).
addPathPatterns("/**"). // 拦截所有 url
excludePathPatterns("/user/login"). //不拦截登录注册接口
excludePathPatterns("/user/reg").
excludePathPatterns("/login.html").
excludePathPatterns("/reg.html").
excludePathPatterns("/**/*.js").
excludePathPatterns("/**/*.css").
excludePathPatterns("/**/*.png").
excludePathPatterns("/**/*.jpg");
}
}
要求
在 1.3 中已经创建了自定义拦截器 和 将自定义拦截器添加到系统配置中,并设置拦截的规则
(1)下面创建登录和首页的 html

(2)创建 controller 包,在包中创建 UserController,写登录页面和首页的业务代码
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/login")
public boolean login(HttpServletRequest request,String username, String password) {
boolean result = false;
if (StringUtils.hasLength(username) && StringUtils.hasLength(password)) {
if(username.equals("admin") && password.equals("admin")) {
HttpSession session = request.getSession();
session.setAttribute("userinfo","userinfo");
return true;
}
}
return result;
}
@RequestMapping("/index")
public String index() {
return "Hello Index";
}
}
(3)运行程序,访问页面,对比登录前和登录后的效果


有了拦截器之后,会在调⽤ Controller 之前进⾏相应的业务处理,执⾏的流程如下图所示

实现原理源码分析


通过源码分析,可以看出,Sping 中的拦截器也是通过动态代理和环绕通知的思想实现的
所有请求地址添加 api 前缀,c 表示所有
@Configuration
public class AppConfig implements WebMvcConfigurer {
// 所有的接口添加 api 前缀
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.addPathPrefix("api", c -> true);
}
}

@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/index")
public String index() {
int num = 10/0;
return "Hello Index";
}
}
在 config 包中,创建 MyExceptionAdvice 类
@RestControllerAdvice // 当前是针对 Controller 的通知类(增强类)
public class MyExceptionAdvice {
@ExceptionHandler(ArithmeticException.class)
public HashMap<String,Object> arithmeticExceptionAdvice(ArithmeticException e) {
HashMap<String, Object> result = new HashMap<>();
result.put("state",-1);
result.put("data",null);
result.put("msg" , "算出异常:"+ e.getMessage());
return result;
}
}
也可以这样写,效果是一样的
@ControllerAdvice
public class MyExceptionAdvice {
@ExceptionHandler(ArithmeticException.class)
@ResponseBody
public HashMap<String,Object> arithmeticExceptionAdvice(ArithmeticException e) {
HashMap<String, Object> result = new HashMap<>();
result.put("state",-1);
result.put("data",null);
result.put("msg" , "算数异常:"+ e.getMessage());
return result;
}
}

如果再有一个空指针异常,那么上面的代码是不行的,还要写一个针对空指针异常处理器
@ExceptionHandler(NullPointerException.class)
public HashMap<String,Object> nullPointerExceptionAdvice(NullPointerException e) {
HashMap<String, Object> result = new HashMap<>();
result.put("state",-1);
result.put("data",null);
result.put("msg" , "空指针异常异常:"+ e.getMessage());
return result;
}
@RequestMapping("/index")
public String index(HttpServletRequest request,String username, String password) {
Object obj = null;
System.out.println(obj.hashCode());
return "Hello Index";
}

但是需要考虑的一点是,如果每个异常都这样写,那么工作量是非常大的,并且还有自定义异常,所以上面这样写肯定是不好的,既然是异常直接写 Exception 就好了,它是所有异常的父类,如果遇到不是前面写的两种异常,那么就会直接匹配到 Exception
当有多个异常通知时,匹配顺序为当前类及其⼦类向上依次匹配
@ExceptionHandler(Exception.class)
public HashMap<String,Object> exceptionAdvice(Exception e) {
HashMap<String, Object> result = new HashMap<>();
result.put("state",-1);
result.put("data",null);
result.put("msg" , "异常:"+ e.getMessage());
return result;
}
可以看到优先匹配的还是前面写的 空指针异常

给当前类添加 @ControllerAdvice
实现 ResponseBodyAdvice 重写其方法
supports 方法,此方法表示内容是否需要重写(通过此⽅法可以选择性部分控制器和方法进行重写),如果要重写返回 true
beforeBodyWrite 方法,方法返回之前调用此方法
@ControllerAdvice
public class MyResponseAdvice implements ResponseBodyAdvice {
// 返回一个 boolean 值,true 表示返回数据之前对数据进行重写,也就是会进入 beforeBodyWrite 方法
// 返回 false 表示对结果不进行任何处理,直接返回
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
// 方法返回之前调用此方法
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
HashMap<String,Object> result = new HashMap<>();
result.put("state",1);
result.put("data",body);
result.put("msg","");
return result;
}
}
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/login")
public boolean login(HttpServletRequest request,String username, String password) {
boolean result = false;
if (StringUtils.hasLength(username) && StringUtils.hasLength(password)) {
if(username.equals("admin") && password.equals("admin")) {
HttpSession session = request.getSession();
session.setAttribute("userinfo","userinfo");
return true;
}
}
return result;
}
@RequestMapping("/reg")
public int reg() {
return 1;
}
}

通过对 @ControllerAdvice 源码的分析我们可以知道上面统一异常和统一数据返回的执行流程
(1)先看 @ControllerAdvice 源码

可以看到 @ControllerAdvice 派生于 @Component 组件而所有组件初始化都会调用 InitializingBean 接口
(2)下面查看 initializingBean 有哪些实现类
在查询过程中发现,其中 Spring MVC 中的实现子类是 RequestMappingHandlerAdapter,它里面有一个方法 afterPropertiesSet()方法,表示所有的参数设置完成之后执行的方法

(3)而这个方法中有一个 initControllerAdviceCache 方法,查询此方法

发现这个方法在执行时会查找使用所有的 @ControllerAdvice 类,发送某个事件时,调用相应的 Advice 方法,比如返回数据前调用统一数据封装,比如发生异常是调用异常的 Advice 方法实现的
我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i
Rackup通过Rack的默认处理程序成功运行任何Rack应用程序。例如:classRackAppdefcall(environment)['200',{'Content-Type'=>'text/html'},["Helloworld"]]endendrunRackApp.new但是当最后一行更改为使用Rack的内置CGI处理程序时,rackup给出“NoMethodErrorat/undefinedmethod`call'fornil:NilClass”:Rack::Handler::CGI.runRackApp.newRack的其他内置处理程序也提出了同样的反对意见。例如Rack
为什么4.1%2返回0.0999999999999996?但是4.2%2==0.2。 最佳答案 参见此处:WhatEveryProgrammerShouldKnowAboutFloating-PointArithmetic实数是无限的。计算机使用的位数有限(今天是32位、64位)。因此计算机进行的浮点运算不能代表所有的实数。0.1是这些数字之一。请注意,这不是与Ruby相关的问题,而是与所有编程语言相关的问题,因为它来自计算机表示实数的方式。 关于ruby-为什么4.1%2使用Ruby返
我将应用程序升级到Rails4,一切正常。我可以登录并转到我的编辑页面。也更新了观点。使用标准View时,用户会更新。但是当我添加例如字段:name时,它不会在表单中更新。使用devise3.1.1和gem'protected_attributes'我需要在设备或数据库上运行某种更新命令吗?我也搜索过这个地方,找到了许多不同的解决方案,但没有一个会更新我的用户字段。我没有添加任何自定义字段。 最佳答案 如果您想允许额外的参数,您可以在ApplicationController中使用beforefilter,因为Rails4将参数
我有一个包含多个键的散列和一个字符串,该字符串不包含散列中的任何键或包含一个键。h={"k1"=>"v1","k2"=>"v2","k3"=>"v3"}s="thisisanexamplestringthatmightoccurwithakeysomewhereinthestringk1(withspecialcharacterslike(^&*$#@!^&&*))"检查s是否包含h中的任何键的最佳方法是什么,如果包含,则返回它包含的键的值?例如,对于上面的h和s的例子,输出应该是v1。编辑:只有字符串是用户定义的。哈希将始终相同。 最佳答案
所以我开始关注ruby,很多东西看起来不错,但我对隐式return语句很反感。我理解默认情况下让所有内容返回self或nil但不是语句的最后一个值。对我来说,它看起来非常脆弱(尤其是)如果你正在使用一个不打算返回某些东西的方法(尤其是一个改变状态/破坏性方法的函数!),其他人可能最终依赖于一个返回对方法的目的并不重要,并且有很大的改变机会。隐式返回有什么意义?有没有办法让事情变得更简单?总是有返回以防止隐含返回被认为是好的做法吗?我是不是太担心这个了?附言当人们想要从方法中返回特定的东西时,他们是否经常使用隐式返回,这不是让你组中的其他人更容易破坏彼此的代码吗?当然,记录一切并给出
只是想确保我理解了事情。据我目前收集到的信息,Cucumber只是一个“包装器”,或者是一种通过将事物分类为功能和步骤来组织测试的好方法,其中实际的单元测试处于步骤阶段。它允许您根据事物的工作方式组织您的测试。对吗? 最佳答案 有点。它是一种组织测试的方式,但不仅如此。它的行为就像最初的Rails集成测试一样,但更易于使用。这里最大的好处是您的session在整个Scenario中保持透明。关于Cucumber的另一件事是您(应该)从使用您的代码的浏览器或客户端的角度进行测试。如果您愿意,您可以使用步骤来构建对象和设置状态,但通常您
我意识到这可能是一个非常基本的问题,但我现在已经花了几天时间回过头来解决这个问题,但出于某种原因,Google就是没有帮助我。(我认为部分问题在于我是一个初学者,我不知道该问什么......)我也看过O'Reilly的RubyCookbook和RailsAPI,但我仍然停留在这个问题上.我找到了一些关于多态关系的信息,但它似乎不是我需要的(尽管如果我错了请告诉我)。我正在尝试调整MichaelHartl'stutorial创建一个包含用户、文章和评论的博客应用程序(不使用脚手架)。我希望评论既属于用户又属于文章。我的主要问题是:我不知道如何将当前文章的ID放入评论Controller。
我在新的Debian6VirtualBoxVM上安装RVM时遇到问题。我已经安装了所有需要的包并使用下载了安装脚本(curl-shttps://rvm.beginrescueend.com/install/rvm)>rvm,但以单个用户身份运行时bashrvm我收到以下错误消息:ERROR:Unabletocheckoutbranch.安装在这里停止,并且(据我所知)没有安装RVM的任何文件。如果我以root身份运行脚本(对于多用户安装),我会收到另一条消息:Successfullycheckedoutbranch''安装程序继续并指示成功,但未添加.rvm目录,甚至在修改我的.bas
为什么以下不同?Time.now.end_of_day==Time.now.end_of_day-0.days#falseTime.now.end_of_day.to_s==Time.now.end_of_day-0.days.to_s#true 最佳答案 因为纳秒数不同:ruby-1.9.2-p180:014>(Time.now.end_of_day-0.days).nsec=>999999000ruby-1.9.2-p180:015>Time.now.end_of_day.nsec=>999999998