> For the complete documentation index, see [llms.txt](https://rowkey-books.gitbook.io/pragmatic-java-engineer/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://rowkey-books.gitbook.io/pragmatic-java-engineer/chapter4-spring/spring-common.md).

# 4.4 Spring常用组件

除了上两节讲述的Spring核心组件和数据操作组件外，Spring还有一些常用的组件。

## 4.4.1 Spring Expression Language

Spring3引入了Spring Expression Language（SpEL），这是一种在运行时给Bean的属性或者构造函数参数注入值的方法。其具有以下优点：

* 可以通过Bean的ID引用Bean；
* 可以调用某个对象的方法或者访问它的属性；
* 支持数学、关系和逻辑操作；
* 正则表达式匹配；
* 支持集合操作

SpEL表达式被#{}包围，跟placeholders中的#{}非常像，最简单的SpEL表达式可以写作#{1}。以下是几个例子。

* \#{T(System).currentTimeMillis()} ： 这个表达式负责获得当前的系统时间，T()操作符负责将java.lang.System解析成类，以便可以调用currentTimeMillis()方法或者访问其静态变量。
* \#{systemProperties\['spring.profile']}： 引用系统属性spring.profile
* \#{adminUser.userName}: 引用id为adminUser的属性userName
* \#{adminUser.getUserName()?.toUpperCase()}: 调用adminUser的getUserName方法,这里的?的作用是防止getUserName的返回值为null。
* \#{adminUser.noteList.!\[title]}: 把adminUser的noteList中每一个note的title都提取出来构成一个新的字符串集合。

引用Spring EL的这些值可以通过@Value注解。

```
public UserService(            

    @Value("#{adminUser.userName}")
    private String adminUserName;
    
    ...
}
```

此外，也可以使用代码做EL解析和执行。

```
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext();  
ctx.refresh();  
ExpressionParser parser = new SpelExpressionParser();  
StandardEvaluationContext context = new StandardEvaluationContext();  
context.setBeanResolver(new BeanFactoryResolver(ctx));  
//context.registerFunction(HASH, hash); //注册方法
//context.setVariable(ARGS, arguments); //注册变量
Properties result = parser.parseExpression("@systemProperties").getValue(context, Properties.class);  
Assert.assertEquals(System.getProperties(), result);  
```

## 4.4.2 Spring Remoting

Spring Web提供了几种远程服务的封装，基本都是一个ServiceExporter用来发布服务，一个ProxyFactoryBean来引用服务。

* RMI：对Java自带RMI机制的封装，包括RmiServiceExporter和RmiProxyFactoryBean。
* Spring HTTP invoker：Spring自己实现的一种基于HTTP协议使用Java的序列化机制的RPC方式。包括HttpInvokerProxyFactoryBean和HttpInvokerServiceExporter
* Hessian：Hessian协议的封装使用，包括HessianProxyFactoryBean和HessianServiceExporter。
* Burlap: Burlap是一种基于XML的RPC协议，包括BurlapProxyFactoryBean和BurlapServiceExporter.

对于这些远程服务封装的使用，基本是类似的：

![](/files/AvFpOobMAnWuGApjq790)

## 4.4.3 Spring与JMX的集成

Java管理扩展（Java Management Extension，JMX）从Java5.0开始引入，提供连接、监控和管理远程JVM的方式。如果一个 Java对象可以由一个遵循 JMX 规范的管理器应用管理，那么这个Java对象就可以称为一个可由JMX管理的资源。使用Spring与JMX集成，实现方式灵活而且简单：

* 可以自动探测实现MBean接口的MBean对象，而且可以将一个普通的Spring Bean注册为MBean；
* 定制管理MBean的接口，根据需要暴露特定管理MBean的操作；
* 使用注解定义MBean管理接口；
* 可以实现对本地和远程MBean的代理。

声明MBean。

```
public interface AdminMBean {
    public void invoke(String className, String methodName, Object[] args);
}

public class Admin implements AdminMBean {
    public void invoke(String className, String methodName, Object[] args) {
      ...
    }
}  
```

配置并暴露服务。

```
<bean id="adminBean" class="me.rowkey.pje.jmx.impl.Admin" />

<bean id="exporter" class="org.springframework.jmx.export.MBeanExporter"
     lazy-init="false">
   <property name="beans">
       <map>
           <entry key="me.rowkey.pje.jmx.impl:name=AdminBean"
                  value-ref="adminBean" />
       </map>
   </property>
</bean>

<bean id="registry" class="org.springframework.remoting.rmi.RmiRegistryFactoryBean">
   <property name="port" value="9999" />
</bean>

<bean id="serverConnector"
     class="org.springframework.jmx.support.ConnectorServerFactoryBean">
   <property name="objectName" value="connector:name=webapprmi" />
   <property name="serviceUrl"
             value="service:jmx:rmi:///jndi/rmi://localhost:9999}/kmconnector" />
</bean>
```

以上便在9999端口开放了JMX协议并发布了一个MBean，其底层通讯是基于RMI的。这样通过serviceUrl："service:jmx:rmi:///jndi/rmi://127.0.0.1:9099/kmconnector"以及mbeanName："suishen.libs.admin.jmx.impl:name=AdminBean"即可调用服务。

## 4.4.4 Spring Quartz

Spring Quartz是Spring自己实现的一套定时调度框架，支持Cron表达式。需要注意的是相比起真正的Quartz框架，Spring Quartz是轻量级的，缺乏分布式等高级特性。

典型的使用Spring Quartz的XML配置如下：

```
<task:scheduler id="quartzScheduler" pool-size="10"/>

<task:scheduled-tasks scheduler="quartzScheduler">

      <task:scheduled ref="userDisableService" method="enableService" cron="* */5 * * * ?"/>

</task:scheduled-tasks>
```

其中task:scheduler配置了Quartz使用的scheduler的线程池大小为10。配置的任务则是每5分钟执行userDisableService这个Bean的enableService方法。

当然，Spring Quartz也支持注解配置，开关如下：

```
<task:annotation-driven scheduler="qbScheduler" mode="proxy"/>
```

当然，也可以使用@注解EnableScheduling来开启Spring Quartz的注解支持。

这样就可以在bean中使用@Scheduled注解来配置定时任务。

```
@Service
public class UserDisableService{
    
    @Scheduled(cron = "* */5 * * * ?")
    public viod enableService(){
        ...
    }
}
```

## 4.4.5 Spring CORS

CORS（Cross-Origin Resource Sharing）是为了解决浏览器中跨域请求的问题。简单的Get请求可以使用JSONP解决，而对于其他稍微复杂的请求则需要后端应用支持CORS。Spring4.2之后提供了@CrossOrigin注解实现对CORS的支持。

* 在Controller方法上配置

  ```
  @CrossOrigin(origins = {"http://localhost:8088"})
  @RequestMapping(value = "/corsTest", method =  RequestMethod.GET)
  public String greetings() {    
      return "cors test";
  }
  ```
* 在Controller上配置，那么此Controller中所有的method都支持CORS

  ```
  @CrossOrigin(origins = "http://localhost:8088", maxAge = 3600)
  @Controller
  @RequestMapping("/api/")
  public class TestController {    
      @RequestMapping(value = "/corsTest", method =  RequestMethod.GET)
      public String greetings() {    
          return "cors test";
      } 
  }
  ```
* Java Config全局配置

  ```
  @Configuration
  @EnableWebMvc
  public class SpringWebConfig extends WebMvcConfigurerAdapter {    
      @Override    
      public void addCorsMappings(CorsRegistry registry) {       
          //对所有URL配置
          //registry.addMapping("/**");
          
          //针对某些URL配置
          registry.addMapping("/api/**").allowedOrigins("http://localhost:8888")            
                                          .allowedMethods("PUT", "DELETE")            
                                          .allowedHeaders("header1", "header2", "header3")           
                                          .exposedHeaders("header1", "header2")           
                                          .allowCredentials(false).maxAge(3600);    
      }
  }
  ```
* XML全局配置

  ```
  <mvc:cors>    
      <!--<mvc:mapping path="/**" />-->
      
      <mvc:mapping path="/api/**" allowed-origins="http://localhost:8888, http://localhost:8088" allowed-methods="GET,POST,HEAD,OPTIONS,PUT,DELETE"
               allowed-headers="Content-Type,X-Requested-With,accept,Origin,Access-Control-Request-Method,Access-Control-Request-Headers" 
               exposed-headers="Access-Control-Allow-Origin,Access-Control-Allow-Credentials"
               allow-credentials="true" max-age="123"/>
  </mvc:cors>
  ```
* Spring Boot中的Filter全局配置

  ```
  @Bean
  public FilterRegistrationBean corsFilter() {
      UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource();
      CorsConfiguration corsConfig = new CorsConfiguration();
      config.setAllowCredentials(true);    
      config.addAllowedOrigin("http://localhost:8888");
      config.addAllowedOrigin("http://localhost:8088");
      config.addAllowedHeader("header1");
      config.addAllowedMethod("GET");
      configSource.registerCorsConfiguration("/**", corsConfig); // CORS 配置对所有接口都有效
      
      FilterRegistrationBean filterBean = new FilterRegistrationBean(new CorsFilter(configSource));
      filterBean.setOrder(0);
      
      return filterBean;
  }
  ```

这里需要注意的是，使用此种方式配置cors，实质上是Spring在handler中加入了一个CorsInterceptor，而这个拦截器其执行是在自定义的拦截器后面的。因此如果跨域请求被前面的拦截器拦住了，那么不会走到CorsInterceptor，会报跨域错误。
