vlambda博客
学习文章列表

读书笔记《building-restful-web-services-with-spring-5-second-edition》AOP和记录器控件

Chapter 9. AOP and Logger Controls

在本章中,我们将学习 Spring Aspect-Oriented ProgrammingAOP< /span>) 和记录器控件,包括它们的理论和实现。我们将把 Spring AOP 集成到我们现有的 REST API 中,并介绍 AOP 和记录器控件如何让我们的生活更轻松。

在本章中,我们将介绍以下主题:

  • Spring AOP theory
  • Implementation of Spring AOP
  • Why do we need logger controls?
  • How do we implement logger controls?
  • Integrating Spring AOP and logger controls

Aspect-oriented programming (AOP)


面向方面的编程是一个概念,我们在不修改代码本身的情况下向现有代码添加新行为。当涉及到日志记录或方法身份验证时,AOP 概念非常有用。

在 Spring 中有很多方法可以使用 AOP。让我们不要太详细,因为这将是一个需要讨论的大话题。在这里,我们将只讨论 @Before 切入点以及如何在我们的业务逻辑中使用 @Before

AOP (@Before) with execution

AOP 中的术语执行意味着在 @Aspect 注释本身中有一个切入点,它不依赖于控制器 API。另一种方法是您必须在 API 调用中明确提及 注释。下个话题讲一下显式切入点:

package com.packtpub.aop;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class TokenRequiredAspect {  
  @Before("execution(* com.packtpub.restapp.HomeController.testAOPExecution())")
  public void tokenRequiredWithoutAnnoation() throws Throwable{
    System.out.println("Before tokenRequiredWithExecution");
  }
}

在这个切入点中,我们使用了 @Before 注释,并且它使用 execution(* com.packtpub.restapp.HomeController.testAOPWithoutAnnotation()),这意味着这个切入点将专注于一个特定的方法, HomeController 类中的 testAOPWithoutAnnotation 方法,在我们的例子中。

对于 AOP 相关的工作,我们可能需要将依赖添加到我们的 pom.xml 文件中,如下所述:

    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.8.13</version>
    </dependency>

前面的依赖将带来所有面向方面的类来支持我们在本章中的 AOP 实现。

Note

@Aspect:这个注解用来使类支持切面。在 Spring 中,方面可以使用 XML 配置或注解来实现,例如 @Aspect@Component:这个注解将使根据 Spring 的组件扫描器规则可扫描的类。通过使用 @Component@Aspect 提及这个类,我们告诉 Spring 扫描这个类并将其识别为一个方面。

HomeController 类的代码如下:

  @ResponseBody
  @RequestMapping("/test/aop/with/execution") 
  public Map<String, Object> testAOPExecution(){
    Map<String, Object> map = new LinkedHashMap<>();
    map.put("result", "Aloha");
    return map;
  }

在这里,我们简单地创建一个新方法来测试我们的 AOP。您可能不需要创建新的 API 来测试我们的 AOP。只要您提供适当的方法名称,就可以了。为了方便读者,我们在 HomeContoller 类中创建了一个名为 testAOPExecution 的新方法。

Testing AOP @Before execution

只需在浏览器中或使用任何其他 REST 客户端调用 API (http://localhost:8080/test/aop/with/execution);然后,您应该在控制台中看到以下内容:

Before tokenRequiredWithExecution

尽管此日志对我们的 business 逻辑没有真正的帮助,但我们将暂时保留它以使读者更容易阅读了解流程。一旦我们了解了 AOP 及其运作方式,我们就会将它集成到我们的业务逻辑中。

AOP (@Before) with annotation

到目前为止,我们已经看到了一种基于执行的 AOP 方法,它可以用于 one 或更多方法。但是,在某些地方,我们可能需要保持实现的简单性以提高可见性。这将帮助我们在需要的地方使用它,并且它不依赖于任何方法。我们称之为基于显式注解的 AOP。

为了使用这个 AOP 概念,我们可能需要创建一个接口来帮助我们实现我们需要实现的目标。

TokenRequired 只是我们的 Aspect 类的基本接口。它将提供给我们的 Aspect 类,如下所述:

package com.packtpub.aop;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface TokenRequired {
}

Note

@Retention:保留策略决定在什么时候应该丢弃注释。在我们的例子中,RetentionPolicy.RUNTIME 将在运行时为 JVM 保留。其他保留策略如下:SOURCE:仅与源代码一起保留,编译时丢弃。代码一旦编译,注解就没有用了,所以不会写入字节码。CLASS:会一直保留到编译时,运行时会被丢弃.@Target:此注解适用于类级别并在运行时匹配。目标注解可用于收集目标对象。

下面的 tokenRequiredWithAnnotation 方法将为我们的切面实现业务逻辑。为了保持逻辑简单,我们刚刚提供了 System.out.println(..)。稍后,我们将主要逻辑添加到方法中:

@Aspect
@Component
public class TokenRequiredAspect {
  // old method (with execution)  
  @Before("@annotation(tokenRequired)")
  public void tokenRequiredWithAnnotation(TokenRequired tokenRequired) throws Throwable{
    System.out.println("Before tokenRequiredWithAnnotation");
  } 
}

在前面的代码中,我们创建了一个名为 tokenRequiredWithAnnotation 的方法,并提供了 TokenRequired 接口作为该方法的参数。我们可以在 @annotation(tokenRequired) 方法的顶部看到名为 @Before 的注解。每次在任何方法中使用 @TokenRequired 注释时都会调用此方法。可以看到注解用法如下:

  @ResponseBody
  @RequestMapping("/test/aop/with/annotation")
  @TokenRequired
  public Map<String, Object> testAOPAnnotation(){
    Map<String, Object> map = new LinkedHashMap<>();
    map.put("result", "Aloha");   
    return map;
  }

之前的 AOP 方法和 this 的主要区别是 @TokenRequired。在旧的 API 调用程序中,我们没有明确提及任何 AOP 注释,但我们必须在此调用程序中提及 @TokenRequired,因为它会调用适当的 AOP 方法。另外,在这个 AOP 方法中,我们不需要提及 execution,就像我们在前面的 execution(* com.packtpub.restapp .HomeController.testAOPWithoutAnnotation()) 方法。

Testing AOP @Before annotation

只需在浏览器中或使用任何其他 REST 客户端调用 API (http://localhost:8080/test/aop/with/annotation);然后,您应该在控制台上看到 following

Before tokenRequiredWithAnnotation

Integrating AOP with JWT

假设您要限制 UserContoller 方法中的 deleteUser 选项。删除用户的人应该拥有正确的 JWT 令牌。如果他们没有令牌,我们不会让 他们 删除任何用户。在这里,我们将首先有一个 packt 主题以 create 令牌。

可以调用 http://localhost:8080/security/generate/token?subject=packt 生成的令牌 API 来生成令牌。

当我们在主题中使用 packt 时,它会生成 eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJwYWNrdCIsImV4cCI6MTUwOTk0NzY2Mn0.hIsVggbam0pRoLOnSe8L9GQS4IFFfFklborwJVthsmz0 令牌。

现在,我们必须创建一个 AOP 方法来限制用户,要求他们在 delete 调用的标头中包含令牌:

@Before("@annotation(tokenRequired)")
public void tokenRequiredWithAnnotation(TokenRequired tokenRequired) throws Throwable{   
       ServletRequestAttributes reqAttributes = (ServletRequestAttributes)RequestContextHolder.currentRequestAttributes();
       HttpServletRequest request = reqAttributes.getRequest();    
       // checks for token in request header
       String tokenInHeader = request.getHeader("token");    
       if(StringUtils.isEmpty(tokenInHeader)){
              throw new IllegalArgumentException("Empty token");
           }    
       Claims claims = Jwts.parser() .setSigningKey(DatatypeConverter.parseBase64Binary(SecurityServiceImpl.secretKey))
       .parseClaimsJws(tokenInHeader).getBody();    
       if(claims == null || claims.getSubject() == null){
                throw new IllegalArgumentException("Token Error : Claim is null");
             }    
       if(!claims.getSubject().equalsIgnoreCase("packt")){
                throw new IllegalArgumentExceptionception("Subject doesn't match in the token");
          }
       }

查看前面的代码,您可以看到 AOP 中的 JWT 集成。是的,我们已经将 JWT 令牌验证部分与 AOP 集成在一起。所以以后,如果有人调用@TokenRequired-annotated API,它会首先来到AOP方法并检查token匹配。如果令牌为空、不匹配或过期,我们将收到错误消息。下面将讨论所有可能的错误。

现在,我们可以在 UserController 类的 API 调用中开始使用 @TokenRequired 注释。因此,每当调用此 deleteUser 方法时,它都会转到 JWT,在执行 API 方法本身之前检查切入点。通过这样做,我们可以确保在没有令牌的情况下不会调用 deleteUser 方法。

UserController 类的代码如下:

  @ResponseBody
  @TokenRequired
  @RequestMapping(value = "", method = RequestMethod.DELETE)
  public Map<String, Object> deleteUser(
      @RequestParam(value="userid") Integer userid){
    Map<String, Object> map = new LinkedHashMap<>();   
    userSevice.deleteUser(userid);   
    map.put("result", "deleted");
    return map;
  }

如果token为空或null,则会抛出以下错误:

{
   "timestamp": 1509949209993,
   "status": 500,
   "error": "Internal Server Error",
   "exception": "java.lang.reflect.UndeclaredThrowableException",
   "message": "No message available",
   "path": "/user"
}

如果令牌匹配,它将显示结果而不会引发任何错误。您将看到以下结果:

{
    "result": "deleted"
} 

如果我们不在标头中提供任何标记,则可能会引发以下错误:

{
   "timestamp": 1509948248281,
   "status": 500,
   "error": "Internal Server Error",
   "exception": "java.lang.IllegalArgumentException",
   "message": "JWT String argument cannot be null or empty.",
   "path": "/user"
}

如果令牌过期,您将收到以下错误:

 {
   "timestamp": 1509947985415,
   "status": 500,
   "error": "Internal Server Error",
   "exception": "io.jsonwebtoken.ExpiredJwtException",
   "message": "JWT expired at 2017-11-06T00:54:22-0500. Current time: 2017-11-06T00:59:45-0500",
   "path": "/test/aop/with/annotation"
} 

Logger controls


我们 需要跟踪特定进程的输出时,日志记录会很有帮助。在将应用程序部署到服务器后出现问题时,它将帮助我们验证过程或找到错误的根本原因。如果没有记录器,如果发生任何事情,将很难跟踪和找出问题。

我们可以在我们的应用程序中使用许多日志框架; Log4j 和 Logback 是大多数应用程序中使用的两个主要框架。

SLF4J, Log4J, and Logback

SLF4j 是一个 API,可帮助我们在部署期间选择 Log4j 或 Logback 或 any 其他 JDK 日志记录。 SLF4j 只是一个抽象层,它为使用我们的日志 API 的用户提供了自由。如果有人想在他们的实现中使用 JDK 日志记录或 Log4j,SLF4j 将帮助他们在运行时插入所需的框架。

如果我们创建了一个不能被某人用作库的最终产品,我们可以直接实现 Log4j 或 Logback。但是,如果 我们 有一个可以用作库的代码,那么使用 SLF4j 会更好,所以用户可以按照他们想要的任何日志记录。

Logback 是 Log4j 的更好替代方案,并为 SLF4j 提供原生支持。

Logback framework

前面我们提到Logback比Log4j更可取;这里我们将讨论如何实现 Logback 日志框架。

Logback 中包含三个模块:

  1. logback-core: Basic logging
  2. logback-classic: Improved logging and SLF4j support
  3. logback-access: Servlet container support

logback-core 模块是 Log4j 框架中其他两个模块的基础。 logback-classic 模块是 Log4j 的改进版本,具有更多功能。此外,logback-classic 模块原生实现了 SLF4j API。由于这种原生支持,我们可以切换到不同的日志框架,例如 Java Util Logging (JUL ) 和 Log4j。

logback-access 模块为 servlet 容器 such 如 Tomcat/ Jetty,专门提供 HTTP 访问日志设施。

Logback dependency and configuration

为了在我们的应用程序中使用 Logback,我们需要 logback-classic 依赖。但是,logback-classic 依赖已经在 spring-boot-starter 依赖中可用。我们可以使用项目文件夹中的依赖树 (mvn dependency:tree) 来检查这一点:

mvn dependency:tree

在检查项目文件夹中的依赖树时,我们将获得所有依赖关系的整个树。下面是我们可以看到 spring-boot-starter 依赖下的 logback-classic 依赖的部分:

[INFO] | +- org.springframework.boot:spring-boot-starter:jar:1.5.7.RELEASE:compile
[INFO] | +- org.springframework.boot:spring-boot:jar:1.5.7.RELEASE:compile
[INFO] | +- org.springframework.boot:spring-boot-autoconfigure:jar:1.5.7.RELEASE:compile
[INFO] | +- org.springframework.boot:spring-boot-starter-logging:jar:1.5.7.RELEASE:compile
[INFO] | | +- ch.qos.logback:logback-classic:jar:1.1.11:compile
[INFO] | | | \- ch.qos.logback:logback-core:jar:1.1.11:compile
[INFO] | | +- org.slf4j:jcl-over-slf4j:jar:1.7.25:compile
[INFO] | | +- org.slf4j:jul-to-slf4j:jar:1.7.25:compile
[INFO] | | \- org.slf4j:log4j-over-slf4j:jar:1.7.25:compile
[INFO] | \- org.yaml:snakeyaml:jar:1.17:runtime
[INFO] +- com.fasterxml.jackson.core:jackson-databind:jar:2

由于必要的依赖文件已经可用,我们不需要为 Logback 框架实现添加任何依赖项。

Logging levels

由于 SLF4j 定义了这些日志记录级别,因此实现 SLF4j 的人应该调整 SFL4j 的日志记录级别。日志记录级别如下:

  • TRACE: Detailed comments that might not be used in all cases
  • DEBUG: Useful comments for debugging purposes in production
  • INFO: General comments that might be helpful during development
  • WARN: Warning messages that might be helpful in specific scenarios such as deprecated methods
  • ERROR: Severe error messages to be watched out for by the developer

让我们将日志配置添加到 application.properties 文件中:

# spring framework logging 
logging.level.org.springframework = ERROR

# local application logging
logging.level.com.packtpub.restapp = INFO

在前面的配置中,我们对 Spring Framework 和我们的应用程序都使用了日志记录配置。根据我们的配置,它将为 Spring Framework 打印 ERROR,为我们的应用程序打印 INFO

Logback implementation in class

让我们在类中添加一个 Logger;在我们的例子中,我们可以使用UserController。我们必须导入 org.slf4j.Loggerorg.slf4j.LoggerFactory。我们可以检查以下代码:

private static final Logger _logger = LoggerFactory.getLogger(HomeController.class);

在前面的代码中,我们引入了 _logger 实例。我们使用 UserController 类作为 _logger 实例的参数。

现在,我们必须使用 _logger 实例来打印我们想要的消息。在这里,我们使用 _logger.info() 来打印消息:

package com.packtpub.restapp;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
// other imports
@RestController
@RequestMapping("/")
public class HomeController {  
  private static final Logger _logger = LoggerFactory.getLogger(HomeController.class);  
  @Autowired
  SecurityService securityService;  
  @ResponseBody
  @RequestMapping("")
  public Map<String, Object> test() {
    Map<String, Object> map = new LinkedHashMap<>();
    map.put("result", "Aloha");    
    _logger.trace("{test} trace");
    _logger.debug("{test} debug");
    _logger.info("{test} info");
    _logger.warn("{test} warn ");
    _logger.error("{test} error");    
    return map;
  }

在前面的代码中,我们使用了各种记录器来打印消息。当您重新启动服务器并调用 http://localhost:8080 REST API 时,您将在控制台中看到以下输出:

2018-01-15 16:29:55.951 INFO 17812 --- [nio-8080-exec-1] com.packtpub.restapp.HomeController : {test} info
2018-01-15 16:29:55.951 WARN 17812 --- [nio-8080-exec-1] com.packtpub.restapp.HomeController : {test} warn
2018-01-15 16:29:55.951 ERROR 17812 --- [nio-8080-exec-1] com.packtpub.restapp.HomeController : {test} error

从日志中可以看出,类名会一直在日志中,以标识日志中的具体类。由于我们没有提到任何日志记录模式,记录器采用默认模式来打印类的输出。如果需要,我们可以更改配置文件中的模式以获取自定义日志记录。

在前面的代码中,我们使用了不同的日志记录级别来打印消息。日志级别是有限制的,所以根据业务需求和实现,我们必须配置我们的日志级别。

在我们的记录器配置中,我们只使用了控制台打印选项。我们还可以提供一个选项,以便在我们想要的任何地方打印到外部文件。

Summary


在本章中,我们介绍了 Spring AOP 和记录器控件的实现。在我们现有的代码中,我们介绍了 Spring AOP,并介绍了 AOP 如何通过代码重用来节省时间。为了让用户了解 AOP,我们简化了 AOP 的实现。在下一章中,我们将讨论如何构建 REST 客户端并讨论更多关于 Spring 中的错误处理。