vlambda博客
学习文章列表

读书笔记《hands-on-high-performance-with-spring-5》Spring MVC优化

Spring MVC Optimization

上一章中,我们了解了Spring 面向切面编程 (AOP)模块, AOP 概念、各种术语以及如何实现建议。我们还看到了代理概念及其使用代理模式的实现。我们通过遵循最佳实践来实现 Spring AOP 的质量和性能。

Spring MVC 是当今最流行的 Java Web 应用程序框架。它是由 Spring 本身提供的。 Spring Web MVC 有助于开发灵活且松散耦合的基于 Web 的应用程序。 Spring MVC 遵循 Model-View-Controller (MVC) 模式,将输入逻辑、业务逻辑和表示逻辑分离,同时提供组件之间的松耦合. Spring MVC 模块允许我们在不使用 Web 应用程序中的请求和响应对象的情况下编写测试用例。因此,它消除了在企业应用程序中测试 Web 组件的开销。 Spring MVC 还支持多种新的视图技术并允许扩展。 Spring MVC 为控制器、视图解析器、处理程序映射和 POJO bean 提供了清晰的角色定义,这使得创建 Java Web 应用程序变得简单。

在本章中,我们将学习以下主题:

  • Spring MVC configuration
  • Spring asynchronous processing, @Async annotation
  • CompletableFuture with Spring Async
  • Spring Security configuration
  • Authentication cache
  • Fast and stateless API authentication with Spring Security
  • Monitoring and managing Tomcat with JMX
  • Spring MVC performance improvements

Spring MVC configuration

Spring MVC 架构与前端控制器 servlet,DispatcherServlet 一起设计,它是前端控制器模式实现,充当所有 HTTP 请求和响应的入口点。 DispatcherServlet 可以使用 Java 配置或在部署描述符文件 web.xml 中进行配置和映射。在进入配置部分之前,让我们了解一下 Spring MVC 架构的流程。

Spring MVC architecture

在 Spring MVC 框架中,有多个核心组件维护请求和响应执行的流程。这些组件分得很清楚,有不同的接口和实现类,可以根据需要使用。这些核心组件如下:

组件

总结

DispatcherServlet

它通过 HTTP 请求和响应的生命周期充当 Spring MVC 框架的前端控制器。

处理程序映射

当请求到来时,该组件负责决定哪个控制器将处理 URL。

控制器

它执行业务逻辑并将结果数据映射到 ModelAndView 中。

模型和视图

它根据执行结果和要渲染的视图对象保存模型数据对象。

ViewResolver

它决定要渲染的视图。

查看

它显示来自模型对象的结果数据。

下图说明了 Spring MVC 架构中上述组件的流程:

读书笔记《hands-on-high-performance-with-spring-5》Spring MVC优化
Spring MVC architecture

让我们了解架构的基本流程:

  1. When the incoming request comes, it is intercepted by the front controller, DispatcherServlet. After intercepting the request, the front controller finds the appropriate HandlerMapping.
  2. The HandlerMapping maps the client request call to the appropriate Controller, based on the configuration file or from the annotation Controller list, and returns the Controller information to the front controller.
  3. The DispatcherServlet dispatches the request to the appropriate Controller.
  4. The Controller executes the business logic defined under the Controller method and returns the resultant data, in the form of ModelAndView, back to the front controller.
  5. The front controller gets the view name based on the values in the ModelAndView and passes it to the ViewResolver to resolve the actual view, based on the configured view resolver.
  6. The view uses the Model object to render the screen. The output is generated in the form of HttpServletResponse and passed to the front controller.
  7. The front controller sends the response back to the servlet container to send the output back to the user.

现在,让我们了解一下 Spring MVC 的配置方法。 Spring MVC 配置可以通过以下方式进行设置:

  • XML-based configuration
  • Java-based configuration

在我们开始使用上述方法进行配置之前,让我们定义设置 Spring MVC 应用程序所涉及的步骤:

  1. Configuring front controller
  2. Creating Spring application context
  3. Configuring ViewResolver

XML-based configuration

在基于 XML 的配置中,我们将使用 XML 文件在外部进行 Spring MVC 配置。让我们按照前面的步骤继续进行配置。

Configuring front controller

要在基于 XML 的配置中配置前端控制器 servlet,DispatcherServlet,我们需要在 web.xml 文件中添加以下 XML 代码:

  <servlet>
    <servlet-name>spring-mvc</servlet-name>
    <servlet-class>
      org.springframework.web.servlet.DispatcherServlet
    </servlet-class>
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>/WEB-INF/spring-mvc-context.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>

  <servlet-mapping>
    <servlet-name>spring-mvc</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>

在前面的 XML 代码中,我们首先配置了 DispatcherServlet。然后,我们提到了上下文配置位置,/WEB-INF/spring-mvc-context.xml。我们将 load-on-startup 值设置为 1,因此 servlet 容器将在启动时加载此 servlet。在第二部分中,我们定义了 servlet-mapping 标记以将 URL / 映射到 DispatcherServlet。现在,我们将在下一步中定义 Spring 应用程序上下文。

It is good to configure the load-on-startup element under the DispatcherServlet configuration to load it at the highest priority. This is because, in a cluster environment, you might face timeout issues if Spring is not up and you get a large number of calls hitting your web app once it's deployed.

Creating a Spring application context

web.xml 中配置好 DispatcherServlet 之后,让我们继续创建 Spring 应用程序上下文。为此,我们需要在 spring-mvc-context.xml 文件中添加以下 XML 代码:

<beans>
<!-- Schema definitions are skipped. -->
<context:component-scan base- package="com.packt.springhighperformance.ch4.controller" />
<mvc:annotation-driven />
</beans>

在前面的 XML 代码中,我们首先为 com.packt.springhighperformance.ch4.controller 定义了一个组件扫描标签 包,以便创建和自动装配所有 bean 和控制器。

然后,我们使用 来注册 自动不同的bean和组件,包括请求映射、数据绑定、验证和自动转换功能与 @ResponseBody

Configuring ViewResolver

要配置ViewResolver,我们需要在spring-mvc-context.xml文件中为InternalResourceViewResolver类指定一个bean,之后<mvc:注解驱动/>。让我们这样做:

<beans>
<!-- Schema definitions are skipped. -->
<context:component-scan base- package="com.packt.springhighperformance.ch4.controller" />
<mvc:annotation-driven />

<bean class="org.springframework.web.servlet.view.InternalResourceViewResolv er">
    <property name="prefix">
      <value>/WEB-INF/views/</value>
    </property>
    <property name="suffix">
      <value>.jsp</value>
    </property>
  </bean>
</beans>

配置ViewResolver后,我们将创建一个Controller来测试配置。但是,在继续之前,让我们看看基于 Java 的配置。

Java-based configuration

对于基于 Java 的 Spring MVC 配置,我们将遵循与基于 XML 的配置相同的步骤。在基于 Java 的配置中,所有配置都将在 Java 类下完成。让我们按照顺序。

Configuring front controller

在 Spring 5.0 中,通过实现或扩展以下三个类中的任何一个,可以通过三种方式以编程方式配置 DispatcherServlet

  • WebAppInitializer interface
  • AbstractDispatcherServletInitializer abstract class
  • AbstractAnnotationConfigDispatcherServletInitializer abstract class

我们将使用 AbstractDispatcherServletInitializer 类,因为它是使用基于 Java 的 Spring 配置的应用程序的首选方法。它是首选,因为它允许我们启动 servlet 应用程序上下文以及根应用程序上下文。

我们需要创建以下类来配置 DispatcherServlet

import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

public class SpringMvcWebInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

  @Override
  protected Class<?>[] getRootConfigClasses() { return null; } @Override protected Class<?>[] getServletConfigClasses() {
    return new Class[] { SpringMvcWebConfig.class };
  }

  @Override
  protected String[] getServletMappings() {
    return new String[] { "/" };
  }
}

前面的类代码等价于我们在基于XML的配置部分创建的web.xml文件配置。在前面的类中,getRootConfigClasses() 方法用于指定根应用程序上下文配置类(或 null,如果不需要)。 getServletConfigClasses() 用于指定 Web 应用程序配置类(或 null,如果不需要)。 getServletMappings() 方法用于指定 DispatcherServlet 的 servlet 映射。将首先加载根配置类,然后加载 servlet 配置类。根配置类将创建一个 ApplicationContext,它将充当父上下文,而 servlet 配置类将创建一个 WebApplicationContext,它将充当父上下文的子上下文语境。

Creating a Spring application context and configuring a ViewResolver

在 Spring 5.0 中,要创建 Spring 应用程序上下文并使用 Java 配置配置 ViewResolver,我们需要在类中添加以下代码:

@Configuration
@EnableWebMvc
@ComponentScan({ "com.packt.springhighperformance.ch4.bankingapp.controller"})
public class SpringMvcWebConfig implements WebMvcConfigurer {

  @Bean
  public InternalResourceViewResolver resolver() {
    InternalResourceViewResolver resolver = new 
    InternalResourceViewResolver();
    resolver.setPrefix("/WEB-INF/views/");
    resolver.setSuffix(".jsp");
    return resolver;
  }

}

在前面的代码中,我们创建了一个类 SpringMvcWebConfig,实现了一个 WebMvcConfigurer 接口,该接口提供了自定义 Spring MVC 配置的选项。 @EnableWebMvc 对象启用 Spring MVC 的默认配置。 @ComponentScan 对象指定要扫描控制器的基本包。 @EnableWebMvc@ComponentScan 这两个注解等价于 我们在 XML-based configuration 部分的 spring-mvc-context.xml 中创建. resolve() 方法返回 InternalResourceViewResolver,这有助于从预配置目录映射逻辑视图名称。

Creating a controller

现在,让我们创建一个控制器类来映射 /home 请求,如下所示:

package com.packt.springhighperformance.ch4.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class BankController {

  @RequestMapping(value = "/home")
  public String home() {
    return "home";
  }
}

在上述代码中,@Controller 定义了一个包含请求映射的 Spring MVC 控制器。 @RequestMapping(value = "home") 对象定义了一个映射 URL,/home,到一个方法,home()。因此,当浏览器遇到 /home 请求时,它会执行 home() 方法。

Creating a view

现在,让我们在 src/main/webapp/WEB-INF/views/home.jsp 文件夹中创建一个视图 home.jsp,其中包含以下 HTML 内容:

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>Spring MVC</title>
</head>
<body>
  <h2>Welcome to Bank</h2>
</body>
</html>

现在,当我们运行这个应用程序时,它将显示以下输出:

读书笔记《hands-on-high-performance-with-spring-5》Spring MVC优化

在下一节中,我们将学习 Spring 异步处理。

Spring asynchronous processing, @Async annotation

Spring 提供对异步方法执行的支持。这也可以使用线程来实现,但它会使代码更复杂,有时会导致更多的错误和错误。当我们需要以异步方式执行一个简单的动作时,使用线程来处理它是一个繁琐的过程。在某些情况下,需要异步执行操作,例如将消息从一台机器发送到另一台机器。异步处理的主要优点是调用者不必等待被调用方法的完成。为了在单独的线程中执行方法,您需要使用 @Async 批注对该方法进行批注。

可以通过使用 @EnableAsync 注释在后台线程池中运行 @Async 方法来启用异步处理支持。以下是启用异步处理的 Java 配置示例:

@Configuration
@EnableAsync
public class SpringAppAsyncConfig { ... }

也可以使用 XML 配置启用异步处理,如下所示:

<task:executor id="myappexecutor" pool-size="10" />
<task:annotation-driven executor="myappexecutor"/>

@Async annotation modes

@Async注解处理方式有两种模式:

  • Fire and forget mode
  • Result retrieval mode

Fire and forget mode

在这种模式下,方法将被配置为 void 类型,以异步运行:

@Async
public void syncCustomerAccounts() {
    logger.info("Customer accounts synced successfully.");
}

Result retrieval mode

在这种模式下,方法将通过使用 Future 类型包装结果来配置返回类型:

@Service
public class BankAsyncService {
  
  private static final Logger LOGGER = 
  Logger.getLogger(BankAsyncService.class);
  
  @Async
    public Future<String> syncCustomerAccount() throws 
    InterruptedException {
    LOGGER.info("Sync Account Processing Started - Thread id: " + 
    Thread.currentThread().getId());
    
    Thread.sleep(2000);
    
    String processInfo = String.format("Sync Account Processing 
    Completed - Thread Name= %d, Thread Name= %s", 
    Thread.currentThread().getId(), 
    Thread.currentThread().getName());
    
    LOGGER.info(processInfo);
    
    return new AsyncResult<String>(processInfo);
    }
}

Spring 还提供了对 AsyncResult 类的支持,该类实现了 Future 接口。它可以用来跟踪异步方法调用的结果。

Limitations of @Async annotation

@Async 注解以下限制:

  • The method needs to be public so that it can be proxied
  • Self-invocation of the asynchronous method would not work, because it bypasses the proxy and calls the underlying method directly

Thread pool executor

您可能想知道我们如何声明异步方法将使用的线程池。默认情况下,对于线程池,Spring 将尝试查找上下文中定义的唯一 TaskExecutor bean 或Executor< /kbd> bean,命名为 TaskExecutor。如果前面两个选项都不能解析,Spring会使用SimpleAsyncTaskExecutor 来处理异步方法处理。

但是,有时我们不想为所有应用程序的任务使用同一个线程池。我们可以有不同的线程池,每种方法都有不同的配置。为此,我们只需将执行程序名称传递给每个方法的 @Async 注解。

要启用异步支持,@Async 注解是不够的;我们需要在配置类中使用 @EnableAsync 注释。

In Spring MVC, when we configure DispatcherServlet using the AbstractAnnotationConfigDispatcherServletInitializer initializer class, which extends AbstractDispatcherServletInitializer, it has the isAsyncSupported flag enabled by default.

现在,我们需要为异步方法调用声明一个线程池定义。在 Spring MVC 基于 Java 的配置中,这可以通过覆盖 Spring Web MVC 配置类中 WebMvcConfigurer 接口的 configureAsyncSupport() 方法来完成。让我们重写这个方法,如下:

@Override
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
    ThreadPoolTaskExecutor t = new ThreadPoolTaskExecutor();
        t.setCorePoolSize(10);
        t.setMaxPoolSize(100);
        t.setThreadNamePrefix("BankAccountSync");
        t.initialize();
        configurer.setTaskExecutor(t);
}

在前面的方法中,我们通过重写 configureAsyncSupport() 方法来配置线程池执行器。现在,我们使用控制器类调用服务类BankAsyncService中创建的异步方法,如下:

@Controller
public class BankController {
  
  private static final Logger LOGGER = Logger.getLogger(BankAsyncService.class);
  
  @Autowired
  BankAsyncService syncService;

  @RequestMapping(value = "/syncacct")
  @ResponseBody
  public Callable<String> syncAccount() {
    LOGGER.info("Entering in controller");

    Callable<String> asyncTask = new Callable<String>() {

      @Override
      public String call() throws Exception {
        Future<String> processSync = syncService.syncCustomerAccount();
        return processSync.get();
      }
    };

    LOGGER.info("Leaving from controller");
    return asyncTask;
  }
}

在前面的示例中,当我们请求 /syncacct 时,它将调用 syncAccount() 并在单独的线程中返回异步方法的结果。

CompletableFuture with Spring Async

CompletableFuture 类是在 Java 8 中引入的,它提供了一种编写异步、多线程、非阻塞代码的简单方法。使用 Spring MVC,还可以将 CompletableFuture 与控制器、服务和存储库一起使用,这些方法来自使用 @Async 注释的公共方法。 CompletableFuture 实现了 Future 接口,该接口提供异步计算的结果。

我们可以通过以下简单的方式创建 CompletableFuture

CompletableFuture<String> completableFuture = new CompletableFuture<String>();

为了得到这个CompletableFuture的结果,我们可以调用CompletableFuture.get()方法。此方法将被阻止,直到 Future 完成。为此,我们可以手动调用 CompletableFuture.complete() 方法来 complete Future

completableFuture.complete("Future is completed")

runAsync() – running a task asynchronously

当我们想要异步执行后台活动任务并且不想从该任务返回任何内容时,我们可以使用 CompletableFuture.runAsync() 方法。它将参数作为 Runnable 对象并返回 CompletableFuture 类型。

让我们尝试通过在 BankController 类中创建另一个控制器方法来使用 runAsync() 方法,示例如下:

@RequestMapping(value = "/synccust")
  @ResponseBody
  public CompletableFuture<String> syncCustomerDetails() {
    LOGGER.info("Entering in controller");

    CompletableFuture<String> completableFuture = new 
    CompletableFuture<>();
    CompletableFuture.runAsync(new Runnable() {
      
      @Override
      public void run() {
        try {           
           completableFuture.complete(syncService.syncCustomerAccount()
           .get());
        } catch (InterruptedException | ExecutionException e) {
          completableFuture.completeExceptionally(e);
        }
        
      }
    }); 
      LOGGER.info("Leaving from controller");
      return completableFuture;
  }

在前面的示例中,当请求带有 /synccust 路径时,它将在单独的线程中运行 syncCustomerAccount() 并完成任务而不返回任何值。

supplyAsync() – running a task asynchronously, with a return value

当我们想要异步完成任务后返回结果时,可以使用CompletableFuture.supplyAsync()。它以 Supplier 作为参数并返回 CompletableFuture

让我们通过在 BankController 类中创建另一个控制器方法来检查 supplyAsync() 方法,示例如下:

@RequestMapping(value = "/synccustbal")
  @ResponseBody
  public CompletableFuture<String> syncCustomerBalance() {
    LOGGER.info("Entering in controller");

    CompletableFuture<String> completableFuture = 
    CompletableFuture.supplyAsync(new Supplier<String>() {
      
      @Override
      public String get() {
        try {
          return syncService.syncCustomerBalance().get();
        } catch (InterruptedException | ExecutionException e) {
          LOGGER.error(e);
        }
        return "No balance found";
      }
    }); 
      LOGGER.info("Leaving from controller");
      return completableFuture;
  }

CompletableFuture 对象使用全局线程池 ForkJoinPool.commonPool() 在单独的线程中执行任务。我们可以创建一个线程池并将其传递给 runAsync()supplyAsync() 方法。

以下是 runAsync()supplyAsync() 方法的两个变体:

CompletableFuture<Void> runAsync(Runnable runnable)
CompletableFuture<Void> runAsync(Runnable runnable, Executor executor)
CompletableFuture<U> supplyAsync(Supplier<U> supplier)
CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)

Attaching a callback to the CompletableFuture

CompletableFuture.get() 阻塞对象并等待,直到 Future 任务完成并返回结果。要构建一个异步系统,应该有一个回调,在 Future 任务完成时自动调用。我们可以使用 thenApply()thenAccept()thenRun() 将回调附加到 CompletableFuture方法。

Spring Security configuration

Spring Security 是一个广泛使用的基于 Java EE 的企业应用程序的安全服务框架。在身份验证级别,Spring Security 提供了不同类型的身份验证模型。其中一些模型是由第三方提供的,一些认证特性是由 Spring Security 自己提供的。 Spring Security 提供了以下一些身份验证机制:

  • Form-based authentication
  • OpenID authentication
  • LDAP specifically used in large environments
  • Container-managed authentication
  • Custom authentication systems
  • JAAS

让我们看一个在 Web 应用程序中激活 Spring Security 的示例。我们将使用内存配置。

Configuring Spring Security dependencies

要在 Web 应用程序中配置 Spring Security,我们需要将以下 Maven 依赖项添加到我们的 Project Object Model (POM) 文件中:

<!-- spring security -->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-web</artifactId>
    <version>${spring.framework.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
    <version>${spring.framework.version}</version>
</dependency>

Configuring a security filter for incoming requests

在 Web 应用程序中实现安全性时,最好验证所有传入的请求。在 Spring Security 中,框架本身会查看传入的请求并根据提供的访问权限对用户进行身份验证以执行操作。要拦截到 Web 应用程序的所有传入请求,我们需要配置 filterDelegatingFilterProxy,它将请求委托给 Spring 管理的 bean、FilterChainProxy

<filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>
        org.springframework.web.filter.DelegatingFilterProxy
    </filter-class>
</filter>
<filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

根据 filter 配置,所有的请求都会经过这个 filter。现在,让我们配置与安全相关的东西,例如身份验证、URL 安全和角色访问。

Configuring Spring Security

现在,我们将通过创建 Spring Security 配置类来配置 Spring Security 身份验证和授权,如下所示:

@EnableWebSecurity
public class SpringMvcSecurityConfig extends WebSecurityConfigurerAdapter {
  
  @Autowired
  PasswordEncoder passwordEncoder;
  
  @Override
  protected void configure(AuthenticationManagerBuilder auth)       
  throws   
  Exception {
    auth
    .inMemoryAuthentication()
    .passwordEncoder(passwordEncoder)
    .withUser("user").password(passwordEncoder.encode("user@123"))
    .roles("USER")
    .and()
    .withUser("admin").password(passwordEncoder.
    encode("admin@123")        
    ).roles("USER", "ADMIN");
  }
  
  @Bean
  public PasswordEncoder passwordEncoder() {
      return new BCryptPasswordEncoder();
  }
  
  @Override
  protected void configure(HttpSecurity http) throws Exception {
     http.authorizeRequests()
    .antMatchers("/login").permitAll()
    .antMatchers("/admin/**").hasRole("ADMIN")
    .antMatchers("/**").hasAnyRole("ADMIN","USER")
    .and().formLogin()
    .and().logout().logoutSuccessUrl("/login").permitAll()
    .and()
    .csrf().disable();
  }
}

让我们理解前面的配置:

  • @EnableWebSecurity: It enables Spring Security's web security support, and also provides the Spring MVC integration.
  • WebSecurityConfigurerAdapter: It provides a set of methods that are used to enable specific web security configuration.
  • protected void configure(AuthenticationManagerBuilder auth): We have used in-memory authentication in this example. It can be used to connect to the database using auth.jdbcAuthentication(), or to a Lightweight Directory Access Protocol (LDAP) using auth.ldapAuthentication().
  • .passwordEncoder(passwordEncoder): We have used the password encoder BCryptPasswordEncoder.
  • .withUser("user").password(passwordEncoder.encode("user@123")): It sets the user ID and encoded password for authentication.
  • .roles("USER"): It assigns roles to the user.
  • protected void configure(HttpSecurity http): It is used to secure different URLs that need security.
  • .antMatchers("/login").permitAll(): It permits all of the users to access the login page.
  • .antMatchers("/admin/**").hasRole("ADMIN"): It permits access to the admin panel to the users who have the ADMIN role.
  • .antMatchers("/**").anyRequest().hasAnyRole("ADMIN", "USER"): It means that to make any request with "/", you must be logged in with the ADMIN or USER role.
  • .and().formLogin(): It will provide a default login page, with username and password fields.
  • .and().logout().logoutSuccessUrl("/login").permitAll(): It sets the logout success page when a user logs out.
  • .csrf().disable(): By default, the Cross Site Request Forgery (CSRF) flag is enabled. Here, we have disabled it from configuration.

Adding a controller

我们将使用以下 BankController 类进行 URL 映射:

@Controller
public class BankController {
  
  @GetMapping("/")
  public ModelAndView home(Principal principal) {
    ModelAndView model = new ModelAndView();
    model.addObject("title", "Welcome to Bank");
    model.addObject("message", "Hi " + principal.getName());
    model.setViewName("index");
    return model;
  }
  
  @GetMapping("/admin**")
  public ModelAndView adminPage() {
    ModelAndView model = new ModelAndView();
    model.addObject("title", "Welcome to Admin Panel");
    model.addObject("message", "This is secured page - Admin 
    Panel");
    model.setViewName("admin");
    return model;
  }
  
  @PostMapping("/logout")
  public String logout(HttpServletRequest request, 
  HttpServletResponse 
  response) {
    Authentication auth = 
    SecurityContextHolder.getContext().getAuthentication();
    if (auth != null) {
      new SecurityContextLogoutHandler().logout(request, response, 
      auth);
      request.getSession().invalidate();
    }
    return "redirect:/login";
  }
}

现在,当我们运行这个示例时,它会首先显示 Spring Framework 提供的登录验证表单,然后再尝试访问 Web 应用程序的任何 URL。如果用户使用 USER 角色登录并尝试访问管理面板,他们将被限制访问它。如果用户使用 ADMIN 角色登录,他们将能够访问用户面板和管理面板。

Authentication cache

当应用程序的调用次数达到最大时,Spring Security 性能成为主要关注点之一。默认情况下,Spring Security 为每个新请求创建一个新会话,并且每次都准备一个新的安全上下文。这在维护用户身份验证时会成为开销,因此会降低性能。

例如,我们有一个 API 要求对每个请求进行身份验证。如果对该 API 进行了多次调用,则会影响使用该 API 的应用程序的性能。所以,让我们在没有缓存实现的情况下理解这个问题。看看下面的日志,我们使用 curl 命令调用 API,没有缓存实现:

curl -sL --connect-timeout 1 -i http://localhost:8080/authentication-cache/secure/login -H "Authorization: Basic Y3VzdDAwMTpUZXN0QDEyMw=="

看看下面的日志:

21:53:46.302 RDS DEBUG JdbcTemplate - Executing prepared SQL query
21:53:46.302 RDS DEBUG JdbcTemplate - Executing prepared SQL statement [select username,password,enabled from users where username = ?]
21:53:46.302 RDS DEBUG DataSourceUtils - Fetching JDBC Connection from DataSource
21:53:46.302 RDS DEBUG SimpleDriverDataSource - Creating new JDBC Driver Connection to [jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false]
21:53:46.307 RDS DEBUG DataSourceUtils - Returning JDBC Connection to DataSource
21:53:46.307 RDS DEBUG JdbcTemplate - Executing prepared SQL query
21:53:46.307 RDS DEBUG JdbcTemplate - Executing prepared SQL statement [select username,authority from authorities where username = ?]
21:53:46.307 RDS DEBUG DataSourceUtils - Fetching JDBC Connection from DataSource
21:53:46.307 RDS DEBUG SimpleDriverDataSource - Creating new JDBC Driver Connection to [jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false]
21:53:46.307 RDS DEBUG DataSourceUtils - Returning JDBC Connection to DataSource

每次我们调用此 API 时,它都会使用数据库值验证用户名和密码。这会影响应用程序的性能,如果用户频繁调用,可能会导致不必要的负载。

克服此问题的一个有尊严的解决方案是将用户身份验证缓存到特定的时间限制。 We 将使用带有正确配置的 AuthenticationProviderUserCache 的实现,并将其传递给 < /span>AuthenticationManagerBuilder。我们将使用 EhCache 来处理缓存的对象。 我们可以通过以下步骤来使用这个解决方案:

  1. Implementing the caching configuration class
  2. Providing UserCache to AuthenticationProvider
  3. Providing AuthenticationProvider to AuthenticationManagerBuilder

Implementing the caching configuration class

我们创建了以下类,它将提供 UserCache bean,该 bean 将把它提供给 AuthenticationProvider

@Configuration
@EnableCaching
public class SpringMvcCacheConfig {

  @Bean
  public EhCacheFactoryBean ehCacheFactoryBean() {
    EhCacheFactoryBean ehCacheFactory = new EhCacheFactoryBean();
    ehCacheFactory.setCacheManager(cacheManagerFactoryBean()
    .getObject());
    return ehCacheFactory;
  }

  @Bean
  public CacheManager cacheManager() {
    return new         
    EhCacheCacheManager(cacheManagerFactoryBean().getObject());
  }

  @Bean
  public EhCacheManagerFactoryBean cacheManagerFactoryBean() {
    EhCacheManagerFactoryBean cacheManager = new 
    EhCacheManagerFactoryBean();
    return cacheManager;
  }

  @Bean
  public UserCache userCache() {
    EhCacheBasedUserCache userCache = new EhCacheBasedUserCache();
    userCache.setCache(ehCacheFactoryBean().getObject());
    return userCache;
  }
}

在前面的类中,@EnableCaching 启用缓存管理。

Providing UserCache to AuthenticationProvider

现在,我们将创建的 UserCache bean 提供给 AuthenticationProvider

@Bean
public AuthenticationProvider authenticationProviderBean() {
     DaoAuthenticationProvider authenticationProvider = new              
     DaoAuthenticationProvider();
     authenticationProvider.setPasswordEncoder(passwordEncoder);
     authenticationProvider.setUserCache(userCache);
     authenticationProvider.
     setUserDetailsService(userDetailsService());
     return authenticationProvider;
}

Providing AuthenticationProvider to AuthenticationManagerBuilder

现在,让我们在 Spring Security 配置类中为 AuthenticationManagerBuilder 提供 AuthenticationProvider

@Autowired
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws     
    Exception {
        
       auth
         .eraseCredentials(false)
         //Providing AuthenticationProvider to 
          AuthenticationManagerBuilder.
         .authenticationProvider(authenticationProviderBean())
         .jdbcAuthentication()
         .dataSource(dataSource); 
    }

现在,让我们调用该 API 并检查身份验证的性能。如果我们调用 API 四次,会产生如下日志:

22:46:55.314 RDS DEBUG EhCacheBasedUserCache - Cache hit: false; username: cust001
22:46:55.447 RDS DEBUG JdbcTemplate - Executing prepared SQL query
22:46:55.447 RDS DEBUG JdbcTemplate - Executing prepared SQL statement [select username,password,enabled from users where username = ?]
22:46:55.447 RDS DEBUG DataSourceUtils - Fetching JDBC Connection from DataSource
22:46:55.447 RDS DEBUG SimpleDriverDataSource - Creating new JDBC Driver Connection to [jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false]
22:46:55.463 RDS DEBUG DataSourceUtils - Returning JDBC Connection to DataSource
22:46:55.463 RDS DEBUG JdbcTemplate - Executing prepared SQL query
22:46:55.463 RDS DEBUG JdbcTemplate - Executing prepared SQL statement [select username,authority from authorities where username = ?]
22:46:55.463 RDS DEBUG DataSourceUtils - Fetching JDBC Connection from DataSource
22:46:55.463 RDS DEBUG SimpleDriverDataSource - Creating new JDBC Driver Connection to [jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false]
22:46:55.479 RDS DEBUG DataSourceUtils - Returning JDBC Connection to DataSource
22:46:55.603 RDS DEBUG EhCacheBasedUserCache - Cache put: cust001
22:47:10.118 RDS DEBUG EhCacheBasedUserCache - Cache hit: true; username: cust001
22:47:12.619 RDS DEBUG EhCacheBasedUserCache - Cache hit: true; username: cust001
22:47:14.851 RDS DEBUG EhCacheBasedUserCache - Cache hit: true; username: cust001

正如您在前面的日志中看到的,最初,AuthenticationProvider 从缓存中搜索 UserDetails 对象;如果它无法从缓存中获取它,AuthenticationProvider 将在数据库中查询 UserDetails 并将更新的对象放入缓存中,对于以后的所有调用,它将检索缓存中的 UserDetails 对象。

If you update the password for a user and try to authenticate the user with the new password, and it fails to match the value in the cache, then it will query the UserDetails from the database.

Fast and stateless API authentication with Spring Security

Spring Security 还为保护非浏览器客户端(例如移动应用程序或其他应用程序)提供了无状态 API。我们将学习如何配置 Spring Security 以保护无状态 API。此外,我们还将弄清楚在设计安全解决方案和提高用户身份验证性能时需要考虑的重点。

API authentication with the JSESSIONID cookie

对于 API 客户端来说,使用基于表单的身份验证并不是一个好的做法,因为必须为请求链提供 JSESSIONID cookie。 Spring Security 还提供了使用 HTTP 基本身份验证的选项,这是一种较旧的方法,但工作正常。在 HTTP 基本身份验证方法中,用户/密码详细信息需要与请求标头一起发送。让我们看一下以下 HTTP 基本身份验证配置的示例:

@Override
protected void configure(HttpSecurity http) throws Exception {
      http
        .authorizeRequests()
        .anyRequest().authenticated()
        .and()
        .httpBasic();
}

在前面的示例中,configure() 方法来自 WebSecurityConfigurerAdapter 抽象类,它提供了该方法的默认实现。子类应该通过调用 super 来调用此方法,因为它可能会覆盖它们的配置。这种配置方法有一个缺点;每当我们调用安全端点时,它都会创建一个新会话。让我们通过使用 curl 命令调用端点来检查这一点:

C:\>curl -sL --connect-timeout 1 -i http://localhost:8080/fast-api-spring-security/secure/login/ -H "Authorization: Basic Y3VzdDAwMTpDdXN0QDEyMw=="
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Set-Cookie: JSESSIONID=B85E9773E6C1E71CE0EC1AD11D897529; Path=/fast-api-spring-security; HttpOnly
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: text/plain;charset=ISO-8859-1
Content-Length: 19
Date: Tue, 27 Mar 2018 18:07:43 GMT

Welcome to the Bank

我们有一个会话 ID cookie;让我们再次调用它:

C:\>curl -sL --connect-timeout 1 -i http://localhost:8080/fast-api-spring-security/secure/login/ -H "Authorization: Basic Y3VzdDAwMTpDdXN0QDEyMw=="
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Set-Cookie: JSESSIONID=14FEB3708295324482BE1DD600D015CC; Path=/fast-api-spring-security; HttpOnly
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: text/plain;charset=ISO-8859-1
Content-Length: 19
Date: Tue, 27 Mar 2018 18:07:47 GMT

Welcome to the Bank

如您所见,我们在每个响应中有两个不同的会话 ID。在前面的示例中,出于测试目的,我们发送了带有编码的用户名和密码的 Authorization 标头。当您点击 URL 时,您可以通过提供用户名和密码进行身份验证,从浏览器获取 Basic Y3VzdDAwMTpDdXN0QDEyMw== 标头值。

API authentication without the JSESSIONID cookie

由于 API 客户端认证不需要会话,我们可以通过以下配置轻松摆脱会话 ID:

@Override
protected void configure(HttpSecurity http) throws Exception {
      http
      .sessionManagement()
        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and()
        .authorizeRequests()
        .anyRequest().authenticated()
        .and()
        .httpBasic();
}

如您所见,在前面的配置中,我们使用了 SessionCreationPolicy.STATELESS。使用此选项,将不会在响应标头中添加会话 cookie。让我们看看更改后会发生什么:

C:\>curl -sL --connect-timeout 1 -i http://localhost:8080/fast-api-spring-security/secure/login/ -H "Authorization: Basic Y3VzdDAwMTpDdXN0QDEyMw=="
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: text/plain;charset=ISO-8859-1
Content-Length: 19
Date: Tue, 27 Mar 2018 18:24:32 GMT

Welcome to the Bank

在前面的示例中,在响应标头中没有找到会话 cookie。因此,通过这种方式,我们可以使用 Spring Security 管理 API 的无状态身份验证。

Monitoring and managing Tomcat with JMX

Java 管理扩展 (JMX) 提供了一种强大的机制来监视和管理 Java 应用程序。可以在 Tomcat 中启用它来监控线程、CPU 使用率和堆内存,以及配置 MBeans。 Spring 提供开箱即用的 JMX 支持,我们可以使用它轻松地将 Spring 应用程序集成到 JMX 架构中。

JMX 支持提供以下核心功能:

  • Easy and flexible support for controlling the management interface of beans
  • Declarative support for exposing MBeans over remote connectors
  • Automatic registration of Spring beans as JMX MBean
  • Simplified support to proxy both local and remote MBean resources

JMX 功能具有三个级别:

  • Instrumentation level: This level contains the components and resources that are represented by one or more Java beans, which are known as managed beans, or MBean.
  • Agent level: This is known as an intermediate agent, called the MBean server. It gets the request from the remote management level and passes it to the appropriate MBean. It can also receive notifications related to state changes from MBeans and forward them back to the remote management level.
  • Remote management level: This layer is made of connectors, adapters, or client programs. It sends requests to the agent level and receives the responses to the requests. Users can connect to the MBean server using either a connector or a client program, such as JConsole, with a protocol such as Remote Method Invocation (RMI) or Internet Inter-ORB Protocol (IIOP), and use an adapter.

简而言之,远程管理级别的用户向代理级别发送请求,代理级别在检测级别找到适当的 MBean,并将响应发送回用户。

Connecting JMX to monitor Tomcat

要在 Tomcat 上配置 JMX,我们需要在 JVM 启动时设置相关的系统属性。我们可以使用以下方法。

我们可以更新 {tomcat-folder}\bin\ 中的 catalina.shcatalina.bat 文件,添加以下值:

-Dcom.sun.management.jmxremote 
-Dcom.sun.management.jmxremote.port={port to access} 
-Dcom.sun.management.jmxremote.authenticate=false 
-Dcom.sun.management.jmxremote.ssl=false

例如,我们可以在 {tomcat-folder}\bin\catalina.bat 中添加以下值:

set JAVA_OPTS="-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=8990
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false"

如果您想在 Eclipse 中为您的 Tomcat 配置 JMX,您需要执行以下操作:

  1. Go to Window | Show View | Server.
  2. Open the Tomcat Overview configuration window by double-clicking on Tomcat v8.0 Server at localhost.
  3. Under General Information, click on Open launch configuration.
  4. Select the Arguments tab of Edit launch configuration properties.
  5. In VM arguments, add the following properties, and then click OK:
-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=8990
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false

进行此更改后,我们需要重新启动 Tomcat 服务器。之后,我们需要测试与 JConsole 的连接。打开JConsole后,我们需要给Remote Process提供一个主机名和端口号,如下:

读书笔记《hands-on-high-performance-with-spring-5》Spring MVC优化

在前面的屏幕截图中,我们提供了主机名 localhost 和端口号 8990。当你点击Connect,你会得到一个对话框,你需要点击Insecure connection,然后你会连接到 JConsole。

Creating an MBean

要创建 MBean,我们可以使用 @Managed 注释将任何类转换为 MBean。 BankTransferService 类将金额从一个帐户转移到另一个帐户。我们将使用这个例子来进一步理解:

@Component
@ManagedResource(objectName = "com.packt.springhighperformance.ch4.mbeans : name=BankMoneyTransferService", description = "Transfers money from one account to another")
public class BankMoneyTransferService {

  private Map<String, Integer> accountMap = new HashMap<String, Integer>();
   {
    accountMap.put("12345", 20000);
    accountMap.put("54321", 10000);
   };

  @ManagedOperation(description = "Amount transfer")
  @ManagedOperationParameters({
      @ManagedOperationParameter(name = "sourceAccount", description = 
       "Transfer from account"),
      @ManagedOperationParameter(name = "destinationAccount",         
        description = "Transfer to account"),
      @ManagedOperationParameter(name = "transferAmount", 
      description = 
        "Amount to be transfer") })
  public void transfer(String sourceAccount, String     
  destinationAccount, int transferAmount) {
    if (transferAmount == 0) {
      throw new IllegalArgumentException("Invalid amount");
    }
    int sourceAcctBalance = accountMap.get(sourceAccount);
    int destinationAcctBalance = accountMap.get(destinationAccount);

    if ((sourceAcctBalance - transferAmount) < 0) {
      throw new IllegalArgumentException("Not enough balance.");
    }
    sourceAcctBalance = sourceAcctBalance - transferAmount;
    destinationAcctBalance = destinationAcctBalance + transferAmount;

    accountMap.put(sourceAccount, sourceAcctBalance);
    accountMap.put(destinationAccount, destinationAcctBalance);
  }

  @ManagedOperation(description = "Check Balance")
  public int checkBalance(String accountNumber) {
    if (StringUtils.isEmpty(accountNumber)) {
      throw new IllegalArgumentException("Enter account no.");
    }
    if (!accountMap.containsKey(accountNumber)) {
      throw new IllegalArgumentException("Account not found.");
    }
    return accountMap.get(accountNumber);
  }

}

在前面的类中,@ManagedResource注解会将该类标记为MBean,而@ManagedAttribute@ManagedOperation注解可以用来暴露任何属性或方法。 @Component 注释将确保所有使用 @Component@Service@Repository 注释的类将被添加到 Spring 上下文中。

Exporting an MBean in a Spring context

现在,我们需要在 Spring 应用程序上下文中创建一个 MBeanExporter。我们只需要在 Spring 上下文 XML 配置中添加以下标记:

<context:mbean-export/>
We need to add the component-scan element before the ‹context:mbean-export/› element; otherwise, the JMX server will not be able to find any beans.

因此,我们的 Spring 上下文配置将如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<beans><!-- Skipped schema definitions -->

  <context:component-scan base- package="com.packt.springhighperformance.ch4.mbeans" /> 
  
<context:mbean-export/>

</beans>

现在,我们只需要启动我们的 Tomcat 服务器并打开 JConsole 来查看我们的 MBean。连接到 JConsole 后,转到 MBeans 选项卡,您可以在其中看到我们的包文件夹,其中包含我们的 BankMoneyTransferService< span> MBean, 在侧边栏中列出:

读书笔记《hands-on-high-performance-with-spring-5》Spring MVC优化

正如您在前面的示例中所见,我们的 MBean 已生成并列在 JConsole 中。现在,我们可以通过点击 Transfer 按钮,调用我们在我们的MBean。当我们点击 checkBalance 按钮时,它将根据输入的帐号在弹出窗口中显示当前余额。在后台,它将调用 BankMoneyTransferService 类的 checkBalance() 方法。

Spring MVC performance improvements

Spring MVC 应用程序性能可以通过多种策略和技巧来提高。在这里,我们列出了一些可以极大提高性能的策略:

  • High performance using connection pooling
  • Hibernate improvements
  • Testing improvements
  • Proper server maintenance
  • Using the authentication cache with Spring Security
  • Implementing the Executor service framework

High performance using connection pooling

在 Spring MVC 中提高性能的最重要特性之一是连接池。在这种机制中,N 个数据库连接在一个池中创建和管理,以提高应用程序的性能。当应用程序需要使用连接时,它只是请求一个连接,使用它,然后将其返回到池中。此过程的主要优点是连接池中可以立即使用连接,因此可以立即使用它们。池本身处理连接的生命周期,因此开发人员不必等待连接建立。

Hibernate improvements

另一个提高性能的重点是关于 Hibernate。脏检查是 Hibernate 提供的功能之一。在脏检查中,Hibernate 会自动区分一个对象是否被修改,是否需要更新。每当需要时,Hibernate 都会做一些繁琐的工作来关注性能成本。当特定实体具有包含大量列的对应表时,成本会增加。为了最小化脏检查成本,我们可以将事务设置为 readOnly,这将提高性能并消除对任何脏检查的需要:

@Transactional(readOnly=true)
public void performanceTestMethod() {
    ....
}

另一个与 Hibernate 相关的改进是间歇性地刷新和清除 Hibernate 会话。 在数据库中插入/修改数据时,Hibernate 会存储已在其会话中持久化的实体版本,以防万一它们在会话关闭之前再次更新。我们可以限制 Hibernate 在其会话中存储实体的时间超过实际需要的时间。插入数据后,我们不再需要将实体存储在持久状态中。因此,我们可以安全地刷新和清除 entityManager 以将实体的状态与数据库同步并从缓存中删除实体。这将使应用程序远离内存限制,并且肯定会对性能产生积极影响:

entityManager.flush();
entityManager.clear();

使用延迟初始化可以进行另一项改进。如果我们使用 Hibernate,我们应该确保正确使用延迟初始化功能。如果需要,我们应该只对实体使用延迟加载。例如,如果我们有一个自定义实体集合,如 Set ,它被配置为延迟初始化,那么该集合的每个实体都将使用单独的查询单独加载。因此,如果一个集合中有多个延迟初始化的实体,那么将有大量的查询按顺序执行,这会严重影响性能。

Testing improvements

为了测试改进,我们可以构建一个可以执行应用程序的测试环境,并在其中获取结果。我们可以编写可重复的性能测试脚本,同时关注绝对性能(如页面渲染时间)和规模性能(如加载时性能下降)。我们可以在我们的测试环境中使用分析器。

Proper server maintenance

一个主要的性能方面与正确的服务器维护有关(如果性能是主要关注点)。以下是提高性能应考虑的一些要点:

  • Cleaning the temporary files periodically by creating a scheduled automated script.
  • Using a load balancer when multiple server instances are running.
  • Optimizing the configuration based on the application needs. For example, in the case of Tomcat, we can refer to Tomcat configuration recommendations.

Using the authentication cache with Spring Security

可以有一个重要的观点来增强性能,这在使用 Spring Security 时可以识别出来。当请求处理时间被测量为不受欢迎时,应正确配置 Spring Security 以提高性能。可能存在实际请求 handling 时间在 100 毫秒左右测量的情况,而 Spring Security 身份验证会额外增加 400-500 毫秒。我们可以使用带有 Spring Security 的身份验证缓存来消除这种性能成本。

Implementing Executor service framework

通过所有可能的改进,如果在请求处理方面保持并发性,则可以提高性能。可能会出现负载测试对我们的应用程序进行多个并发命中的情况,这可能会影响我们应用程序的性能。在这种情况下,我们应该调整 Tomcat 服务器上的线程默认值。如果存在高并发,HTTP 请求将被搁置,直到有线程可用于处理它们。

可以通过在我们的业务逻辑中使用 Executor 框架来扩展默认的服务器线程实现,以在单线程执行流程中从方法内进行并发异步调用。

Summary

在本章中,我们对 Spring MVC 模块有了一个清晰的认识,并了解了不同的配置方法。我们还通过 CompletableFeature 实现了解了 Spring 异步处理概念。之后,我们通过 Spring Security 模块了解了配置。我们还通过无状态 API 了解了 Spring Security 的身份验证部分。然后,我们通过 JMX 浏览了 Tomcat 的监控部分。最后,我们查看了 Spring MVC 的性能改进。

在下一章中,我们将学习 Spring 数据库交互。我们将从具有最佳数据库设计和配置的 Spring JDBC 配置开始。然后,我们将通过最佳连接池配置。我们还将介绍 @Transactional 的概念以提高性能。最后,我们将介绍数据库设计最佳实践。