# 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，会报跨域错误。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://rowkey-books.gitbook.io/pragmatic-java-engineer/chapter4-spring/spring-common.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
