文章目录
Spring
0.测试
对于组件扫描的测试
- JAVA
package soundsystem;
import static org.junit.Assert.*;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
// 告诉JUnit使用 SpringJUnit4ClassRunner 作为测试运行器。
// 该运行器会在测试类启动时加载Spring应用上下文,从而使得我们可以使用Spring的依赖注入、配置文件等功能。
@RunWith(SpringJUnit4ClassRunner.class)
// 告诉Spring如何加载应用上下文。
@ContextConfiguration(classes = CDPlayerConfig.class)
// 在这里,它指定了一个Java配置类——CDPlayerConfig,用于初始化Spring上下文。Spring根据CDPlayerConfig类中的配置来创建和管理Bean。
public class CDPlayerTest {
// 标记该变量需要自动装配
@Autowired
private CompactDisc cd;
@Test
public void cdShouldNotBeNull() {
//断言验证是否成功注入
assertNotNull(cd);
}
}
1.Bean的装配
可选方法
自动化装配
- 使用@Component来标识这个类是一个Bean
- 可以使用@Component(“Bean’s Name”) 来显式的指定这个Bean的id,如果我们不进行显式的指定,比如这个类叫 Ice 那么它的id默认为 ice(也就是类名的第一个字母变为小写)
- JAVA
com.cheng.pojo // 给这个组件命名为ICEEEEEE @Component("ICEEEEEE") public class Ice{ private Double temperature; public bool cold(){ if( temperature < -10) return true; return false; } }
- 使用@ComponentScan来告诉Spring应该在哪些地方进行扫描(即Bean应该在哪找) 如果不给他设置任何属性,它会默认以配置类所在的包作为基础包来扫描组件
- JAVA
com.cheng.config // 这个注解说明它是一个配置类 @Configuration // 这个注解说明它要开启组件扫描,但是要注意,因为没给他配置任何属性,他只会在com.cheng.config包里扫描 @ComponentScan public class WeatherConfig{ }
- XML
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!-- 开启组件扫描,这里显式的指明了要使用soundsystem作为基础包扫描 --> <context:component-scan base-package="soundsystem"/> </beans>
- 需要引入context命名空间以开启组件扫描
- 给@ComponentScan设置基础包
- JAVA
// 这样是给@ComponentScan 的value 属性指定了值 @ComponentScan("pojo") // 指定基础包, 可以指定多个基础包 @ComponentScan(basePackages="pojo") // @ComponentScan(basePackages={"pojo", "servlet"}) // 上面的方法所设置的基础包都是以String类型表示的,这种方法是类型不安全的,如果重构代码,就可能会出现错误 // 还可以指定为包中所包含的类或接口,这些类所在的包将会作为组件扫描的基础包 @ComponentScan(basePackageClasses={Ice.class, LoginServlet.class})
- 使用@Autowired注解实现自动装配(该注解不仅可以使用在类的构造器和Sette方法上,实际上,它可以用在类的任何方法上,只要这个方法需要一个Bean)
- JAVA
@Component public class CDPlayer implements MediaPlayer { private CompactDisc cd; @Autowired public CDPlayer(CompactDisc cd) { this.cd = cd; } @Autowired public void setCompactDisc(CompactDisc cd) { this.cd = cd; } }
- 标记有@Autowired的方法,Spring都会尝试满足方法参数上所声明的依赖,加入有且只有一个Bean匹配依赖需求的话,那么这个Bean将会被装配进来,但是要注意,Spring默认情况下是单例的
- 如果没有匹配的Bean,那么在Spring的应用上下文创还能得时候,Spring将会抛出一个异常,如果我们允许不强制需要Bean来注入,可以将@Autowired的require属性设置为false
XML装配
- 创建XML配置规范
- 最为简单的SpringXML配置如下所示
- XML
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context"> <!-- 在这里编写详细配置 --> </beans>
- 声明一个简单的bean
- XML
<bean class="soundsystem.SgtPeppers" />
- 这里创建这个bean的类是通过class属性来指定的,并且要使用全限定的类名
- 因为没有明确给定id, 所以这个bean将会根据全限定类名来进行命名,在本例中,bean的id将会是"soundsystem.SgtPeppers#0"
- 显然,这样的名字对于我们稍后要进行的应用操作是没有什么用处的,因此我们最好给我们的bean指定一个名字
- XML
<bean id="compactDisc" class="soundsystem.SgtPeppers" />
- 稍后将这个bean装配到别的bean之中时,将会用到这个具体的名字
- 要注意:在基于XML的配置中,我们不再直接负责创建SgtPeppers的实例,在基于JavaConfig的配置中,我们是需要这么做的
- 在基于XML的配置中,当Spring发现这个bean元素时,它将会调用SgtPeppers的默认构造器来创建bean
- 并且,如果当你重命名了类,这里就会找不到对应的类,Spring将会报错
- 借助构造器注入初始化bean
- 使用<constructor-arg>元素
- 将对象的引用装配到依赖于它们的其他对象之中
- 假设我们已经声明了SgtPeppers bean,并且这个类实现了CompactDisc接口
- 我们的CDPlayer bean需要一个CompactDisc作为参数进行装配
- XML
<bean id="cdPlayer" class="soundsystem.CDPlayer"> <!-- 这里,通过使用ID来指明要使用的是compactDisc bean,并且ref告知Spring要将一个bean的引用传递到CDPlayer的构造器中 --> <constructor-arg ref="compactDisc" /> </bean>
- 我们的CDPlayer bean需要一个CompactDisc作为参数进行装配
- 假设我们已经声明了SgtPeppers bean,并且这个类实现了CompactDisc接口
- 将字面量注入到构造器中
- 假设我们现在有一个新的实现类
- JAVA
public class Novel { private String title; private String artist; public Novel(String title, String artist) { this.title = title; this.artist = artist; } }
- 我们来对其进行构造器注入
- XML
<bean id="novel" class="com.cheng.pojo.Novel"> <constructor-arg value="The Great Gatsby"/> <constructor-arg value="F. Scott Fitzgerald"/> </bean>
- 注意,这里我们没有使用ref,而是使用了value属性,该属性表明给定的值要以字面量的形式注入到构造器之中
- 假设我们现在有一个新的实现类
- 将集合注入到构造器中
- 同样,我们假设现在有了一个新的实现类
- JAVA
public class GroceryList { String detail; private List<String> items; GroceryList(String detail, List<String> items) { this.detail = detail; this.items = items; } }
- 显然,在注入时,我们必须要提供一个商品列表
- 简单的,我们可以将列表设置为null
- XML
<bean id="grocerylist" class="com.cheng.pojo.GroceryList"> <constructor-arg value="nothing important." /> <constructor-arg><null/></constructor-arg> </bean>
- 当然,我们也可以给一些有效的值
- XML
<bean id="grocerylist" class="com.cheng.pojo.GroceryList"> <constructor-arg value="nothing important." /> <constructor-arg> <list> <value>Apple</value> <value>Banana</value> </list> </constructor-arg> </bean>
- 显然,你还可以使用ref元素替代value,实现bean应用列表的装配
- 假设有这么一个实现类
public class Library { private List<Novel> novels; Library(List<Novel> novels) { this.novels = novels; } }
- 构造器注入
- XML
<bean id="library" class="com.cheng.pojo.Library"> <constructor-arg> <list> <ref bean="novel" /> <ref bean="novel" /> </list> </constructor-arg> </bean>
- XML
- 同样,我们假设现在有了一个新的实现类
- 将对象的引用装配到依赖于它们的其他对象之中
- 使用c-命名空间
- 将对象的引用装配到依赖于它们的其他对象之中
- 首先,要使用c-命名空间,需要在XML的顶部声明其模式,如下所示
- XML
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:c="http://www.springframework.org/schema/c" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> </beans>
- 在c-命名空间和模式声明之后,我们就可以使用它来声明构造器参数了,如下所示
- XML
<bean id="cdPlayer" class="soundsystem.CDPlayer" c:cd-ref="compactDisc" />
- 这里解释一下这个属性名是如何组成而成的
c-命名空间前缀 构造器参数名 注入bean引用 要注入的bean的ID c : cd - ref = "compactDisc"
- 在这里,我们可以将构造器参数名改成使用参数在整个参数列表中的位置信息
- XML
<!-- 这里,_0标记这个参数是构造器中的第一个参数 --> <bean id="cdPlayer" class="soundsystem.CDPlayer" c:_0-ref="compactDisc" />
- 首先,要使用c-命名空间,需要在XML的顶部声明其模式,如下所示
- 将字面量注入到构造器中
- 对于上面的例子中的Novel类,使用c-命名空间进行构造器参数的注入,应该是这样的
- XML
<bean id="novel" class="com.cheng.pojo.Novel" c:_0="The Great Gatsby" c:_1="F. Scott Fitzgerald"> </bean>
- 可以看到,装配字面量于装配引用的区别在于属性名中去掉的-ref后缀
- 对于上面的例子中的Novel类,使用c-命名空间进行构造器参数的注入,应该是这样的
- c-命名空间无法将集合装配到构造器参数中
- 将对象的引用装配到依赖于它们的其他对象之中
- 使用<constructor-arg>元素
- 借助Setter方法实现属性注入
- 使用<property>元素
- 假设我们现在有一个新的实现类
- JAVA
public class Novel { private String title; private String artist; public Novel() { } public void setTitle(String title) { this.title = title; } public void setTitle(String title) { this.artist = artist; } }
- 我们来对其进行构造器注入
- XML
<bean id="novel" class="com.cheng.pojo.Novel"> <property name="title" value="The Great Gatsby"/> <property name="artist" value="F. Scott Fitzgerald"/> </bean>
- <property>元素为属性的Setter方法提供的功能和<constructor-arg>为构造器所提供的功能是一样的
- name属性指定了使用哪个setter方法,在这里,title表示使用setTitle()方法
- 假设我们现在有一个新的实现类
- 使用p-命名空间
- 首先,要使用p-命名空间,需要在XML的顶部声明其模式,如下所示
- XML
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:p="http://www.springframework.org/schema/p" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> </beans>
- XML
- 我们可以使用p-命名空间,按照以下的方式装配title和artist属性
- XML
<bean id="novel" class="com.cheng.pojo.Novel" p:title="The Great Gatsby" p:artist="F. Scott Fitzgerald" />
- 首先,要使用p-命名空间,需要在XML的顶部声明其模式,如下所示
- 由于通过Setter方法实现属性属性注入的流程和构造器方法是类似的,这里不再赘述
- 使用<property>元素
- 最为简单的SpringXML配置如下所示
JAVA装配
- 使用JavaConfig显式装配Spring
- 创建JavaConfig类的关键在于为其添加@Configuration注解,这个注解表明这个类是一个配置类
- 因为这节我们关注于显式配置,因此我们移除@ComponentScan注解,但要注意,自动化配置和XMl配置和Java配置是可以共存的。
- 显式声明Bean
- 声明简单的Bean
- JAVA
@Bean public CompactDisc sgtPeppers() { return new SgtPeppers(); }
- 默认情况下Bean的id和带有@Bean注解的方法名是一样的,在本例中bean的名字将会是sgtPeppers,如果你想给他设置成一个不同的名字的话,可以通过name属性来进行指定
- @Bean(name=“lonelyHeartsClubBand”)
- 在这里,我们可以发挥Java提供的所有功能,只要最终生成一个CompactDisc实例即可
- 声明简单的Bean
- 借助JavaConfig实现注入
- 引用创建Bean的方法
- JAVA
@Bean public CDPlayer cdPlayer() { return new CDPlayer(sgtPeppers()); }
- 这个Bean和sgtPeppers稍微有些区别,在这里并没有使用默认的构造器来构建实例,而是调用了需要传入CompactDisc对象的构造器来创建CDPlayer实例
- 看起来,CompactDisc是通过调用sgtPeppers()得到的,但情况并非完全如此,因为sgtPeppers()方法上添加了@Bean注解,Spring将会拦截所有对它的调用,并确保直接返回该方法所创建的Bean,而不是每次都对其进行实际的调用、
- 而且Spring默认是单例模式,也就是每一个cdPlayer获得的CD光盘都是完全一样的。
- 这种形式还有另一种理解起来比较简单的方式
- JAVA
@Bean // 这里实际的意思是,当你(也就是一个Bean)需要某些东西(可以是另外一个Bean)时,他会自动装配这个Bean到这个方法中,然后,方法体就可以按照合适的方式是来使用它(它需要的Bean,也就是它的依赖) // 而且不用明确应用CompactDisc的@Bean方法(也就是不用像上例那样) public CDPlayer cdPlayer(CompactDisc compactDisc) { return new CDPlayer(compactDisc); }
- 依赖于这种方式,你可以将配置分散到多个配置类,XML,以及自动扫描和装配Bean中,不管CompactDisc是采用什么方式创建出来的,只要Spring中托管的Bean可以返回一个CompactDisc,它就可以将它提供给需要它的Bean
- 当然,你也可以使用Setter风格的DI配置
- JAVA
@Bean public CDPlayer cdPlayer(CompactDisc compactDisc) { CDPlayer cdPlayer = new CDPlayer(compactDisc); cdPlayer.setCompactDisc(compactDisc); return cdPlayer; }
- 最重要的是,带有@Bean注解的方法可以采用任何必要的Java功能来产生bean实例,这里所存在的可能性仅仅受到Java语言的限制
- 引用创建Bean的方法
- 显式声明Bean
- 因为这节我们关注于显式配置,因此我们移除@ComponentScan注解,但要注意,自动化配置和XMl配置和Java配置是可以共存的。
- 创建JavaConfig类的关键在于为其添加@Configuration注解,这个注解表明这个类是一个配置类
导入和混合配置
- 1.在JavaConfig中引用XML配置
- 假设我们现在有三个JavaConfig配置类
- JAVA: CDConfig
@Configuration public class CDConfig { @Bean public CompactDisc compactDisc() { return new SgtPeppers(); } }
- JAVA: CDPlayerConfig
@Configuration // 这里可以直接使用@Import注解导入CDConfig。但是这里我们采用一种更好的方法 public class CDPlayerConfig { @Bean public CDPlayer cdPlayer(CompactDisc compactDisc) { return new CDPlayer(compactDisc); } }
- JAVA: SoundSystemConfig
@Configuration @Import({CDPlayerConfig.class, CDConfig.class}) public class SoundSystemConfig { }
- 这样,我们将所有的配置类都组合到了更高级别的SoundSystemConfig中
- 现在,基于某种原因,我们通过XML配置了一个BlankDisc, 如下所示
- XML: cd-config.xml
<bean id="compactDisc" class="soundsystem.BlankDisc" c:_0="Sgt. Pepper's Lonely Hearts Club Band" c:_1="The Beatles"> <constructor-arg> <list> <value>Sgt. Pepper's Lonely Hearts Club Band</value> <value>With a Little Help from My Friends</value> <value>Lucy in the Sky with Diamonds</value> <value>Getting Better</value> <value>Fixing a Hole</value> </list> </constructor-arg> </bean>
- 如何让Spring同时加载它和其他基于Java的配置呢?
- 答案是使用@ImportReource注解
- 我们可以修改SoundSystemConfig
- JAVA: SoundSystemConfig
@Configuration // 这里没有导入CDConfig.class,防止出现冲突(即有多个可选的Bean) @Import({CDPlayerConfig.class}) @ImportResource("classpath:cd-config.xml") public class SoundSystemConfig { }
- 这样,BlankDisc将会装配到CDPlayer中,此时与它是通过XML配置的没有任何关系
- 假设我们现在有三个JavaConfig配置类
- 2.在XML配置中引用JavaConfig
-
在XML中引入XML可以使用<import>元素
- XML
<import resource="cd-config.xml" />
-
在XML中引入Java配置可以使用<bean>元素
- XML
<bean class="soundsystem.CDConfig" />
-
2.高级装配
Spring profile
- 如果要在不同的环境(dev,prod,test)中配置使用不同的Bean,可以配置Spring profile
- 声明Bean所在的环境,只需使用@Profile注解,或者通过<beans>的profile属性。
- JAVA
@Configuration public class DataSourceConfig { //这里要注意的是Spring3.1之前,只能在类级别上使用@Profile注解,但从Spring3.2开始,可以在方法级别上使用@Profile注解 @Bean @Profile("dev") public DataSource devDataSource() { return devxxx; } @Bean @Profile("prod") public DataSource prodDataSource() { return prodxxx; } }
- 尽管这两个Bean都被声明在一个profile中,但是只有规定的profile激活时,响应的bean才会被创建
- 没有指定profile的Bean始终都会被创建,与激活哪个profile没有关系
- XML
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd" profile="dev"> <!-- 在这里填写要使用的Bean --> </beans>
- 那么,如何激活某个profile呢?
- 关键有两个属性:spring.profiles.active 和 spring.profiles.default
- 有多种方式来设置这两个属性
- 作为DispatcherServlet的初始化参数
- 作为Web应用的上下文参数
- XML
<?xml version="1.0" encoding="UTF-8"?> <web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"> <context-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/spring/root-context.xml</param-value> </context-param> <!-- 为上下文设置默认的profile --> <context-param> <param-name>spring.profiles.default</param-name> <param-value>dev</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <servlet> <servlet-name>appServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <!-- 为Servlet设置默认的profile --> <param-name>spring.profiles.default</param-name> <param-value>dev</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>appServlet</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> </web-app>
- 作为环境变量
- 作为JVM的系统属性
- 在集成测试类上,使用@ActiveProfiles注解设置
- JAVA
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes={PresistenceTestConfig.class}) @ActiveProfiles("dev") public class PersistenceTest { ... }
- 有多种方式来设置这两个属性
- 关键有两个属性:spring.profiles.active 和 spring.profiles.default
- 在条件化创建Bean方面,profile机制通过基于哪个profile处于激活状态来判断,而Spring4.0提供了一种更为通用的机制来实现条件化的Bean定义
- 这就是使用@Conditional注解定义条件化的bean
- 声明Bean所在的环境,只需使用@Profile注解,或者通过<beans>的profile属性。
条件化的bean声明
- 可以给一个类设置@Conditional注解,注解中只需给定一个实现类Condition接口的类,@Conditional注解会自动调取Condition接口进行条件对比
- JAVA
@Bean @Conditional(MagicExistsCondition.class) public MagicBean magicBean() { return new MagicBean(); }
- JAVA: Condition 接口
public interface Condition { boolean matches(ConditionContext ctxt, AnnotatedTypeMetadata metadata); }
- JAVA: MagicExistsCondition类
public class MagicExistsCondition implements Condition { public boolean matches(ConditionContext ctxt, AnnotatedTypeMetadata metadata) { // 检查环境中是否定义了magic属性,如果存在,则返回true Environment env = context.getEnvironment(); return env.containsProperty("magic"); } }
- JAVA: ConditionContext接口
public interface ConditionContext { // 借助返回的BeanDefinitionRegistry检查bean定义 BeanDefinitionRegistry getRegistry(); // 借助返回的ConfigurableListableBeanFactory检查bean是否存在,甚至探查bean的属性 ConfigurableListableBeanFactory getBeanFactory(); // 借助返回的Environment检查环境变量是否存在以及他的值是什么 Environment getEnvironment(); // 读取并探查返回的ResourceLoader所加载的资源 ResourceLoader getResourceLoader(); // 借助返回的ClassLoader加载并检查类是否存在 ClassLoader getClassLoader(); }
- AnnotatedTypeMetadata接口则能够让我们检查带有@Bean注解的方法上还有什么其他的注解。
处理自动装配的歧义性
- 仅有一个Bean匹配所需的结果时,自动装配才是有效的,因此我们需要手动处理有歧义的Bean
- 有以下两个可选的方法:
- 标识首选的bean
- 可以使用@Primary注解来标识首选的Bean,实际上你所声明的就是“最喜欢“的Bean
- JAVA
@Component // 在自动装配中,需要使用Dessert接口的Bean,都会自动首选装配上IceCream对象。 @Primary public Dessert iceCream() { return new IceCream(); }
- XML
<bean id="iceCream" class="com.dessert.IceCream" primary="true" />
- 当然,如果你设置了多个首选bean,那实际上也就是没有首选bean了。
- 可以使用@Primary注解来标识首选的Bean,实际上你所声明的就是“最喜欢“的Bean
- 限定自动装配的bean
- 可以使用@Qualifier注解来限定所使用的Bean(通常能够达到只有一个Bean满足所规定的限制条件)
- JAVA
@Autowired // 这个注解表明,自动装配要引用的bean要具有String类型的iceCream作为限定符。 // 同时,如果一个Bean没有指定其他的限定符时,Spring会自动给定一个默认的限定符,这个限定符与Bean的id相同 // 也就是说,Spring会将具有iceCream限定符的Bean注入到setDessert方法中,在这里也就是iceCream类 @Qualifier("iceCream") public void setDessert(Dessert dessert) { this.dessert = dessert; }
- 但是要注意的问题是,这样所制定的限定符与要注入的Bean的名称是紧耦合的,对类名称的改动会导致限定符失效
- 当然,我们可以为Bean设置自己的限定符,而不是依赖于将Bean id作为限定符,在这里还是通过@Qualifier注解来做到这一点
- JAVA
@Component // 这样,这个Bean的限定符就变成类cold,而不是跟随类的名称改变而改变,这样就不必担心会破坏自动装配 @Qualifier("cold") public Dessert iceCream() { return new IceCream(); }
- 我们也可以想到,在同一个类上设置多个@Qualifier注解来更加严格的限定一个Bean在多个条件均被满足时才被注入。
- 但Java不允许在同一个条目上出现相同类型的多个注解,但是有简单的解决方法:创还能自定义的限定符注解
- 自定义的限定符注解
- 创建自定义的限定符注解
- JAVA:@Cold
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Qualifier public @interface Cold { }
- JAVA:@Creamy
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Qualifier public @interface Creamy { }
- 当你想使用多个@Qualifier叠加时,就可以像这样类似的创建这些注解,这样,它们本身实际上就成为了限定符注解
- 使用自定义的限定符注解
- JAVA
@Component @Cold @Creamy public Dessert iceCream() { return new IceCream(); }
- JAVA
@Component @Creamy public Dessert otherDessert() { return new OtherDessert(); }
- JAVA
@Autowired // 只有又是冷的,又像奶油的甜品我才注入,这里也就是注入iceCream @Cold @Creamy public void setDessert(Dessert dessert) { this.dessert = dessert; }
- 为了创建自定义的条件化注解,我们创建了一个新的注解并在这个注解上添加了@Conditional。
- 为了创建自定义的限定符注解,我们创建了一个新的注解并在这个注解上添加了@Qualifier。
- 这种技术可以用到很多的Spring注解中,从而能够将他们组合在一起形成特定目标的自定义注解
- 现在让我们来看看如何在不同的作用域中声明Bean
- 创建自定义的限定符注解
- 可以使用@Qualifier注解来限定所使用的Bean(通常能够达到只有一个Bean满足所规定的限制条件)
- 标识首选的bean
- 有以下两个可选的方法:
bean的作用域
- 默认情况下,Spring应用上下文中所有的Bean都是以单例的形式创建的,那么我们如何修改这种默认方式呢
- Spring定义了多种作用域,包括:
- 单例:在整个应用中,只创建Bean的一个实例
- 原型:每次注入或者通过Spring应用上下文获取的时候,都会创建一个新的Bean实例
- 会话:在Web应用中,为每个会话创建一个Bean实例
- 请求:在Web应用中,为每个请求创建一个Bean实例
- 单例是默认的作用域,如果选择其他的作用域,要使用@Scope注解,或者使用<bean>元素的scope属性
- 例如:
- JAVA
@Component // 使用原型模式 @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) // 使用单例模式 // @Scope(ConfigurableBeanFactory.SCOPE_SINGLETON) public NotePad notePad() { return new NotePad(); }
- XML
<bean id="notePad" class="com.cheng.NotePad" scope="prototype" />
- 不论你使用哪种方式来声明原型作用域,每次注入时,都会创建新的实例,也就是每次操作都能得到自己的Notepad实例
- 使用会话和请求作用域
- 在Web应用中,显然,可能会有一个Bean代表用户的购物车,如果购物车使用单例模式,那么所有的用户都会向同一个购物车中添加商品另一方面,如果购物车使用原型模式,会导致在应用的某一个地方往购物车中添加了商品,但在应用的另外一个地方可能就不可用了,因为他获取的又是另外一个全新的购物车了
- 因此,在Web应用中,如果能够实例化在会话和请求范围共享的Bean,就会非常的舒服
- 在这个例子中,使用会话作用域是作为合适的,因为它与给定的用户关联性最大
- 要使用会话作用域,可以使用@Scope注解,具体如下:
- JAVA
@Component @Scope( value=WebApplicationContext.SCOPE_SESSION, proxyMode= ScopedProxyMode.INTERFACES) public ShoppingCart cart() {...}
- 这里,我们将value设置成了WebApplicationContext中的SCOPE_SESSION常量,这会告诉Spring为Web应用中的每个会话创建一个ShoppingCart
- 这会创建多个ShoppingCart Bean的实例,但是对于给定的会话只会创建一个实例,在当前会话相关的操作中,这个Bean实际上相当于单例的
- 在这里,@Scope同时还有一个proxyMode属性,我们先来阐述我们遇到的问题,再来详细解释这个属性
- 何时注入ShoppingCart?
- 因为StoreService是一个单例的Bean,会在Spring应用上下文加载时创建,当它创建时,Spring会试图将ShoppingCart注入到setShoppindCart()方法中
- 但是不要忘记了,ShoppingCart是会话作用域的,知道某个用户进入系统,创建了会话之后,才会出现ShoppingCart实例
- 另外,系统中将会有多个Shopping实例:每个用户一个。因此,我们并不能让Spring注入某个固定的ShoppingCart实例到StoreService中
- 我们实际希望的是:当StoreService处理购物车功能时,它所使用的ShoppingCart实例恰好是当前会话所对应的那一个
- 解决方案
- Spring会注入一个ShoppdingCart的代理,这个代理会暴露于ShoppingCart相同的方法,因此StoreService会认为它就是一个购物车
- 但是当StoreService调用ShoppingCart的方法时,代理会对其进行拦截西并将调用委托给会话作用域内真正的ShoppingCart Bean
- 现在我们来讨论proxyMode属性
- 如配置所示,proxyMode属性被设置成了ScopedProxyMode.INTERFACES,这表明这个代理要实现ShoppingCart接口,并将调用委托给实现Bean
- 如果ShoppingCart是接口而不是类的话,这是可以的,这也是最为理想的代理模式
- 但如果ShoppingCart是一个具体的类的话,Spring就必须使用CGLib来生成基于类的的代理,因此Bean类型是具体类的话,我们必须要将proxyMode属性设置为ScopedProxyMode.TARGET_CLASS,以此来表明要以生成目标类扩展的方式创建代理
- 同理,请求作用域的Bean也应该以作用域代理的方式进行注入
- 当然,我们也可以使用XML来声明会话或请求作用域的Bean,具体如下:
- XML
<bean id="cart" class="com.cheng.ShoppingCart" scope="session"> <aop:scoped-proxy /> </bean>
- <aop:scoped-proxy />和@Score注解的proxyMode属性功能相同,它默认使用CGLib创建目标类的代理,但是我们也可以将proxy-target-class属性设置为false,从而要求它生成基于接口的代理
<aop:scoped-proxy proxy-target-class="false"/>
- 注意,为了使用<aop:scoped-proxy />元素,我们需要在XML配置中声明Spring的aop命名空间
- XML
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation=" http://www.springframework.org/schema/aop http://www.springframework.org/schema/beans/spring-aop.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> </beans>
Spring表达式语言
- 运行时值注入
- 像前文所说的的值装配,实现完全是将值硬编码在实现类中的,如果我们希望这些值在运行时在确定,这样就不符合我们的要求的
- 所幸,Spring给我们提供了两种在运行时求值的方式。
- 属性占位符
- 最简单的方式是声明属性源并通过Spring的Environment来检索属性
- JAVA
@Configuration // 这里默认是在resources/config/app.properties中去查找 @PropertySource("classpath:config/app.properties") public class ExpressiveConfig { @AutoWired Environment env; @Bean public User user() { return new User(env.getProperty("user.username")); } }
- 深入学习Spring的Environment
- getProperty()方法有四个重载的变种形式
- JAVA
String getProperty(String key); String getProperty(String key, String defaultValue); T getProperty(String key, Class<T> type); T getProperty(String key, Class<T> defaultValue);
- 显然,前两种方法允许我们在指定属性不存在的时候使用一个默认值
- 后两种方法与前两种非常类似,区别是他们不会将所有的值都视为String类型。
- JAVA
// 从属性文件中得到的String类型的值自动转换为Interger了 env.getProperty("db.connection.count", Integer.class, 30) ;
- 如果希望某个你要获取的属性值必须被定义,可以使用getRequiredProperty()方法。
- 如果想检查某个属性是否存在,可以使用containsProperty()方法。
- 最后,如果想将属性解析为类的话,可以使用getPropertyAsClass()方法。
- JAVA
Class<CompactDisc> cdClass = env.getPropertyAsClass("disc.class‘, CompactDisc.class);
- getProperty()方法有四个重载的变种形式
- 还可以直接使用 ${…} 的形式直接将外部的属性中的值插入到Spring Bean中
- 例如:
- XML
<!-- 为了使用占位符,我们必须要配置一个PropertySourcesPlaceholderConfigurer Bean,具体如下: --> <context:property-placeholder location="app.properties"> </context:property-placeholder> <bean id="sgtPeppers" class="soundsystem.BlankDisc" c:_title="${disc.title}" c:_arttist="${disc.artist}" />
- 如果我们依赖于组件扫描和自动装配来创建和初始化应用组件的话,可以通过如下方式来指定要注入的值
- JAVA
public BlankDisc( @Value({disc.title}) String title, @Value({disc.artist}) String artist) { this.title = title; this.artist = artist; } // 同理,我们这里也必须要配置一个PropertySourcesPlaceholderConfigurer Bean // 在配置类中添加如下内容 @Bean public static PropertySourcesPlaceholderConfigurer placeholderConfigurer() { return new PropertySourcesPlaceholderConfigurer(); }
- 例如:
- 最简单的方式是声明属性源并通过Spring的Environment来检索属性
- Spring表达式语言(SpEL)
- SpEL表达式需要放在#{ … }之中,这和属性占位符有些类似,但是属性占位符是放在${ … }之中
- 表示字面值
- JAVA
#{3.1415926} #{9.87E6} #{'Hello'} #{false}
- 引用bean,属性和方法
- 假如有一个Bean的ID是sgtPeppers
- 我们可以这样使用这个Bean:
- JAVA
#{sgtPeppers} #{sgtPeppers.artist} #{sgtPeppers.getArtist()} #{sgtPeppers.getArtist().toUpperCase()} // 这里,为了防止getArtist()的返回值为null,我们可以使用类型安全的运算符 ?. // 这个运算符会在访问它右边的内容之前,确保它所对应的元素不是null,即:若sgtPeppers.getArtist()返回值为null, 则不会调用toUpperCase()方法 #{sgtPeppers.getArtist()?.toUpperCase()}
- 在表达式中使用类型
- 如果哦要在SpEL中访问类作用域的方法和常量的话,需要依赖T()这个运算符
- JAVA
T(java.lang.Math)
- 这里所示的T()运算符的结果会是一个Class对象,代表了java.lang.Math,如果需要的话,我们甚至可以把他装配到一个Class类型的Bean属性中
- 但是T()运算符的真正价值在于它能够访问目标类型的静态方法和常量。
- 例如:
- JAVA
#{T(java.lang.Math).PI} #{T(java.lang.Math).random()}
- 如果哦要在SpEL中访问类作用域的方法和常量的话,需要依赖T()这个运算符
- SpEL运算符
- 算数运算: + - * / % ^
- 比较运算: < > == <= >= lt gt eq le ge
- 逻辑运算: and or not |
- 条件运算: ?:
- 正则表达式: matches
- 这里给出一些例子
- JAVA
#{2 * T(java.lang.Math).PI * circle.radius} #{T(java.lang.Math).PI * circle.radius ^ 2} #{disc.title + 'by' + disc.artist} #{counter.total == 100} #{counter.total eq 100} #{scoreboard.score > 1000 ? "Winnner!" : "Loser"} #{disc.title ?: 'Rattle and Hum'} #{admin.email mathces '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.com'}
- 这里给出一些例子
- 计算集合:
- 引用列表中的一个元素
- JAVA
#{songs[4].title} #{songs[T(java.lang.Math).random() * songs.size()].title} #{'This is a test'[3]}
- 使用查询运算符 .?[] 对集合进行过滤,.1 获取第一个匹配项, .$[] 获取最后一个匹配项
- JAVA
// 获取Aerosmith的所有歌曲 #{songs.?[artist eq 'Aerosmith']} // 获取列表中Aerosmith歌曲的第一个匹配项 #{songs.^[artist eq 'Aerosmith']} // 获取列表中Aerosmith歌曲的最后一个匹配项 #{songs.$[artist eq 'Aerosmith']}
- 使用投影运算符 .![] 从集合中的每个成员中选定特定的属性放在另外一个集合中
- JAVA
// 获取Aerosmith的所有歌曲的标题 #{songs.?[artist eq 'Aerosmith'].![title]}
- 引用列表中的一个元素
- 表示字面值
- SpEL表达式需要放在#{ … }之中,这和属性占位符有些类似,但是属性占位符是放在${ … }之中
- 属性占位符
3.面向切面的Spring(AOP)
- Spring借助AspectJ的切点表达式语言来定义Spring切面
- execution() 匹配方法的执行
- 以下均是限制:
- arg()
- @arg()
- this()
- target
- @target()
- within()
- @within()
- @annotation
- 下文将会详细介绍他们的使用方法
编写切点
- 假设我们定义了一个Performance接口
- JAVA
public interface Performance { public void perform(); }
- 以下这个切点表达式能够设置当perform()方法执行时触发某段逻辑(通知)
- JAVA
execution ( * concert.Performce . perform (..)) // 在方法执行时触发 返回任意类型 方法所属的类 方法 使用任意参数 // 即execution后面的括号中指定了一个方法,切点正是设置在这个方法上,当这个方法执行之前(之后)将会触发某段逻辑 // 这里的*表明我们不关心方法返回值的类型 // 这里的(..)表明切点选择任意的perform()方法,而无论该方法的参数是什么
- 假设我们需要配置的切点仅匹配concert包
- JAVA
// 这样,只有在concert包下,并且实现了perform()方法的类在执行perform()方法时会触发通知 execution(* concert.Performace.perform(..)) && within(concert.*)
- 假设我们需要配置的切点仅匹配audience bean
- JAVA
execution(* concert.Performace.perform(..)) && bean('audience') // 或者指定除了特定ID以外的其他Bean应用通知 execution(* concert.Performace.perform(..)) && !bean('audience')
定义切面
- 一个基本的切面大概如下:
- JAVA
// 这个注解表明Audience不仅仅是一个POJO,还是一个切面 @Aspect public class Audience { // 为@Pointcut注解设置的值是一个切点表达式,通过在performance方法上添加@Pointcut,使得我们不必使用这个非常长的切点表达式 // performance()方法的实际内容并不重要,该方法现在实际上已经成了一个标识,表示那个更长的切点表达式 @Pointcut("execution(* concert.Performance.perform(..))") public void performance() {} // 通知方法(即这里的silenceCellPhones()方法)会在目标方法(即任何实现了Performance接口的类的perform()方法)调用之前执行 @Before("performance()") public void silenceCellPhones() { System.out.println("Silencing cell phones"); } // 通知方法(即这里的applause()方法)会在目标方法(即任何实现了Performance接口的类的perform()方法)返回后执行 @AfterReturning("performance()") public void applause() { System.out.println("CLAP CLAP CLAP!!!"); } // 通知方法(即这里的demandRefund()方法)会在目标方法(即任何实现了Performance接口的类的perform()方法)抛出异常后执行 @AfterThrowing("performance()") public void demandRefund() { System.out.println("Demanding a refund"); } // 还有@After:会在目标方法返回后抛出异常后执行 @After("performance()") public void goHome() { System.out.println("Bye Bye..."); } // 因为这里实际上分别定义完整了 @Around, 这里为了避免冲突,就将其注释起来 // 如果同时定义@Before @After @Around 指定的顺序为: // @Around的Before @Before @After @Around的After // 笔者猜测,是因为@Around中手动调用了ProceedingJoinPoint.proceed // 因此先执行@Around中的Before操作,然后因为要执行目标方法,因此执行@Before 目标方法 @After 最后才是 @Around中的After操作 // 对于@Around通知方法,ProceedingJoinPoint对象必须要有,因为我们需要在通知中通过它来调用目标方法 // 当然,我们也可以不调用proceed()方法,这样你的通知就会实际上阻塞目标方法的调用,与之类似,你也可以在通知中对它进行多次调用。 /* @Around("performance()") public void watchPerformance(ProceedingJoinPoint jp) { try{ System.out.println("===Silencing cell phones==="); jp.proceed(); System.out.println("===CLAP CLAP CLAP!!!==="); } catch (Throwable e) { System.out.println("===Demanding a refund==="); } finally { System.out.println("===Bye Bye...==="); } } */ }
- 如果就此止步,Audience只会是Spring容器中的一个bean,即使使用了AspectJ注解
- 如果你使用JavaConfig,可以在配置类的类级别下使用@EnableAspectJAutoProxy注解启动自动代理功能。具体如下:
- JAVA
@Configuration @EnableAspectJAutoProxy @ComponentScan public class ConcertConfig { // 注意,必须要在配置类中将切面声明为Bean,这样Spring才会正常的去装载它,从而使用它其中切面的功能 @Bean public Audience audience() { return new Audience() } }
- 如果你使用XML,请参考如下配置:
- XML
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <context:component-scan base-package="concert" /> <!-- 启用AspectJ自动代理 --> <aop:aspectj-autoproxy /> <!-- 声明Audience bean --> <bean class="concert.Audience" /> </beans>
- 不管你使用的是JavaConfig还是XML,Spring的自动代理都是基于代理的,我们只能限于代理方法的调用。
- 如果想利用AspectJ的所有能力,我们必须在运行时使用AspectJ并且不依赖Spring来创建基于代理的切面
- 处理通知中的参数
- 我们前面提到的perform()方法本身并没有任何参数,但是如果切面所通知的方法确实有参数该怎么办呢?
- 我们能访问和使用传递给被通知方法的参数吗?
- 答案是可以的,只要我们对切点进行一些小小的修改:
- 假设Performace接口发生了一点改变
- JAVA
public interface Performance { public void perform(); public void showDetail(Integer lengthOfTime); }
- 我们想要切面也能够知道这场演出将会持续多久,只需如下编写切点
- JAVA
// 这个注解表明Audience不仅仅是一个POJO,还是一个切面 @Aspect public class Audience { // 这里切点表达式表明匹配的是接受一个Interger参数的showDetail方法,并且使用args限定符来表明传递给showDetail方法的参数也会传递到通知(也就是你希望执行的逻辑)中去 // 这里args中填写的应当是showDetail()方法中声明的参数的参数名,以便Spring进行匹配。 @Pointcut("execution(* concert.Performance.showDetail(Integer)) && args(lengthOfTime)") public void intro(Integer lengthOfTime){} // 这里使用的时候也要带上参数名,通知(这里的introP方法)就可以愉快的使用目标方法传递过来的参数了。 @Before("intro(lengthOfTime)") public void introP(Integer lengthOfTime) { if(lengthOfTime >= 60) System.out.println("It gonna be a great show"); else System.out.println("It will be quick"); } }
- 答案是可以的,只要我们对切点进行一些小小的修改:
通过注解引入新功能
- 利用被称为引用的AOP概念,切面可以为Spring Bean添加新方法
- 假设有的演出由于过于精彩,可以在演出结束后应观众于要求进行返场表演(Encoreable 接口)
- JAVA
public void interface Encoreable { void performEncore(); }
- 现在我们可以访问Performance的所有实现,并对其进行修改,让他们都实现Encoreable接口
- 但是这样有两个问题:
-
- 并非所有的Performance都是具有Encoreable特性的
-
- 当使用第三方实现并且没有源码时无法直接在上面进行修改
-
- 值得庆幸的是,借助AOP的引入功能,我们可以不必在设计上妥协或者侵入性的改变现有的实现
- 为了实现该功能,我们要创建一个新的切面
- JAVA
@Aspect public class EncoreableIntroducer { // 这里,通过使用@DeclareParents注解,我们将Encoreable接口引入到Performance Bean中 // value属性指定了哪种类型的Bean要引入该接口,这里表示所有实现Performance接口的类型(这里的 + 表示是Performance的所有子类型,而不是Performance本身) // defaultImpl属性指定了为引入功能提供实现的类,在这里我们指定是DefaultEncoreable提供实现 // @DeclareParents注解标注的静态属性指明了要引入的接口,在这里,我们所引入的是Encoreable接口 @DeclareParents(value="concert.Performance+", defaultImpl=DefaultEncoreable.class) public static Encoreable encoreable; } @Configuration @EnableAspectJAutoProxy @ComponentScan public class ConcertConfig { @Bean public EncoreableIntroducer encoreableIntroducer() { return new EncoreableIntroducer(); } }
- 现在,我们可以通过以下方式来使用扩展的功能了
- JAVA
@Autowired Performance p; @Test public void TestAspectj() { p.perform(); // 需要先把它强转为后来添加的接口的类型,否则你的编译器就会不给你好果子吃 ((Encoreable) p).performEncore(); }
- 假设有的演出由于过于精彩,可以在演出结束后应观众于要求进行返场表演(Encoreable 接口)
通过XML使用Spring AOP
- 通过注解的切面声明有一个明显的劣势:你必须能够为通知类添加注解。
- 而为了做到这一点,必须要有源码。
- 在前面的内容中,我们基本上认为,基于注解的配置要优于基于Java的配置,基于Java的配置要优于基于XML的配置
- 但是,如果你需要声明切面,但是又不能为通知类添加注解的时候,就必须转向XML配置了(但所幸,它们都是非常类似的)
- Spring的aop命名空间中,提供了如下元素用来在XML中声明切面:
- <aop:advisor>
- <aop:after>
- <aop:after-returning>
- <aop:after-throwing>
- <aop:around>
- <aop:aspect>
- <aop:aspectj-autoproxy>
- <aop:before>
- <aop:config>
- <aop:declare-parents>
- <aop:pointcut>
- 在这里,除了第一个标签我们没有见过,其他的都在通过注解使用AOP中使用过了,我们给出几个例子,相信你就能明白他们如何使用:
- XML:普通通知
<!--顶层的AOP配置元素,大多数的<aop:*>元素必须包含在其中--> <aop:config> <!--定义切点,因为它是在<aop:aspect>元素外定义的,因此可以在多个切面中使用--> <aop:pointcut expression="execution(* concert.Performance.perform(..))" id="performance"/> <!--定义切面,ref属性表示要引用的Bean--> <aop:aspect ref="audience"> <!--表演之前--> <aop:before method="silenceCellPhones" pointcut-ref="performance"/> <!--表演之后--> <aop:after-returning method="applause" pointcut-ref="performance"/> <!--表演失败之后--> <aop:after-throwing method="demandRefund" pointcut-ref="performance"/> </aop:aspect> </aop:config>
- XML:环绕通知
<aop:config> <aop:pointcut expression="execution(* concert.Performance.perform(..))" id="performance"/> <aop:aspect ref="audience"> <!--并没有什么不同,只需要指定一个切点和一个通知方法的名字,但是这个通知方法要遵循我们前文提到过的方式去编写--> <aop:around method="watchPerformance" pointcut-ref="performance"/> </aop:aspect> </aop:config>
- XML:为通知传递参数
<bean class="concert.Audience" id="audience"/> <aop:config> <!--这里,因为是在XML中,因此我们使用and替换&&,同时,lengthOfTime应当和showDetail中的参数名匹配,并且和introP中的参数匹配--> <aop:pointcut expression="execution(* concert.Performance.showDetail(Integer)) and args(lengthOfTime)" id="intro"/> <aop:aspect ref="audience"> <aop:before method="introP" pointcut-ref="intro"/> </aop:aspect> </aop:config>
- XML:通过切面引入新的功能
<aop:config> <aop:aspect> <!-- 这里,types-matching属性表明要匹配Performance接口 --> <!-- 要增加的功能(接口)通过implement-interface属性指定 --> <!-- 最后,通过default-impl属性来指定默认的实现方法,要使用全限定类名来显式指定,指定的不必是一个Spring Bean --> <!-- 还可以通过delegate-ref属性来指定默认的实现方法,但是区别是它指定的必须是一个Spring Bean--> <aop:declare-parents types-matching="concert.Performance+" implement-interface="concert.Encoreable" default-impl="concert.DefaultEncoreable" /> </aop:aspect> </aop:config>
- 最后,我们来聊聊<aop:advisor>标签
- 它不像<aop:aspect>可以有多个切点和多个通知,它只有一个切点和一个通知,具体使用方法如下:
- 首先,需要有一个实现了XXXAdvice接口的Spring Bean
- 这里举一个例子如下:
- JAVA
// 实现了XXXAdvice接口中的XXX方法,就可以在之前/之后执行你的目标逻辑了。 // 这里,MethodBeforeAdvice接口中要实现 void before(Method method, Object[] args, Object target)方法 // 同理,按需求还可以实现AfterAdvice,AfterReturningAdvice接口 // 要实现的方法中提供的参数是有关于目标方法的,可以通过这些参数得到目标方法的一些有关信息 @Component public class Log implements MethodBeforeAdvice, AfterReturningAdvice { //method:要执行的目标对象的方法 //args:参数 //target:目标对象 //目标方法执行前执行 @Override public void before(Method method, Object[] args, Object target) throws Throwable { System.out.println(target.getClass().getName()+"的"+method.getName()+"被执行了"); } //returnValue:返回值 //method:要执行的目标对象的方法 //args:参数 //target:目标对象 //目标方法执行结束后执行 @Override public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable { System.out.println("执行了"+method.getName()+"方法,返回结果为:"+returnValue); } @Autowired public Log() { } }
- 然后,只需要在XML中简单的声明,这个类就生效了
- XML
<aop:config> <!-- 匹配任意public方法--> <!-- 即任意public方法前后都会执行Log类中的before和afterReturning方法 --> <aop:pointcut id="pointcut" expression="execution(public * *(..))"/> <aop:advisor advice-ref="log" pointcut-ref="pointcut"/> </aop:config>
- 它不像<aop:aspect>可以有多个切点和多个通知,它只有一个切点和一个通知,具体使用方法如下:
- Spring的aop命名空间中,提供了如下元素用来在XML中声明切面:
注入AspectJ切面
- 要想在Spring中使用AspectJ,首先得需要安装ajc,也就是AspectJ特殊的编译器
- 这里就不再赘述,详情可以参考这篇博客:https://blog.youkuaiyun.com/festone000/article/details/119874041
- 在项目中配置好AspectJ后,就可以在Spring中愉快的使用AspectJ了(AspectJ与Spring是相互独立的,两个不会相互影响)
- 但是精心设置的切面很可能依赖其他类来完成他们的工作,在Spring中,我们可以借助Spring的依赖注入把Bean装配进AspectJ切面中
- 例如,这是一个AspectJ切面(AspectJ切面后缀为.aj)
- AJ
// 这是一个表演的评论员,他会观看演出并且会在演出之后提供一些批评意见 public aspect CriticAspect { public CriticAspect() {} pointcut performance() : execution(* perform(..)); afterReturning() : performance() { System.out.println(criticismEngine.getCriticism()); } private CriticismEngine criticismEngine; // 注入CriticismEngine public void setCriticismEngine(CriticismEngine criticismEngine) { this.criticismEngine = criticismEngine; } }
- 这是CriticismEngine接口及其简单实现
- JAVA
public interface CriticismEngine { public String getCriticism(); } public class CriticismEngineImpl implements CriticismEngine { public CriticismEngineImpl() {} public String getCriticism() { int i = (int) (Math.random() * criticismPool.length); return criticismPool[i]; } private String[] criticismPool; public void setCriticismPool(String[] criticismPool) { this.criticismPool = criticismPool; } }
- XML
<!-- 将criticismEngine声明为一个Bean,以便注入到切面中 --> <bean id="criticismEngine" class="com.springinaction.springidol.CriticismEngineImpl"> <property name="criticismPool"> <list> <value>Worst performance ever!</value> <value>I laughed, I cried, then I realized I was at the wrong show.</value> <value>A must see show!</value> </list> </property> </bean>
- 为CriticAspect 装配 CriticismEngineImpl
- 首先,由于AspectJ根本不需要Spring就可以织入到我们的应用中,如果要使用Spring的依赖注入为AspectJ切面注入协作者,我们必须先将这个切面声明为一个Bean
- 如下的声明将会把CriticismEngineImpl注入到CriticAspect中
- XML
<!-- 通常情况下,Spring bean由Spring容器初始化,但是AspectJ切面是由AspectJ在运行期创建的--> <!-- 等到Spring有机会为CriticAspect注入CriticismEngine时,CriticAspect已经被实例化了 --> <!-- 因为Spring不能负责创建CriticAspect,但是Spring又需要获取到CriticAspect,于是AspectJ切面都提供了一个静态的aspectOf方法,该方法返回切面的一个单例 --> <!-- 所以为了获得切面的实例,我们必须使用factory-method来调用aspectOf()方法而不是调用CriticAspect的构造器方法 --> <!-- 这里程序按照设定的进行了,但是idea在name="criticismEngine"爆红,显示无法解析属性,不知道是什么原因。 --> <!-- 测试是只要加上factory-method属性才会爆红,猜测是idea的bug --> <bean class="com.aspect.CriticAspect" factory-method="aspectOf"> <property name="criticismEngine" ref="criticismEngine" /> </bean>
- 例如,这是一个AspectJ切面(AspectJ切面后缀为.aj)
4. Spring MVC起步
MVC(一种软件设计模式)
M:Model(模型),负责应用程序的数据和业务逻辑。它代表了应用程序的核心功能,通常包括与数据库交互、处理业务规则、验证数据等任务。模型部分不涉及用户界面,因此它独立于视图和控制器。
V:View(视图),负责显示数据(模型)给用户,并提供用户交互界面。视图部分只关注用户界面展示,通常由HTML、CSS、JavaScript等技术实现。它通过控制器来获取数据,并且根据数据来更新界面。
C:Controller(控制器),负责处理用户输入并对应用程序逻辑进行调度。控制器接收用户的请求,并从模型获取必要的数据,然后选择合适的视图来展示数据。它通常充当模型和视图之间的中介。
DispatcherServlet的配置
-
按照传统的方式,像DispatcherServlet这样的Servlet会配置在web.xml中,但在Spring3.1之后,我们已经可以使用Java将DispatcherServlet配置在Servlet容器中了
-
需要注意的是,这个配置的方法需要Tomcat7会更高版本,因为在这之上的服务器版本才支持Servlet3.0规范
-
具体如下:
- JAVA
package spittr.config; import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer; /** * AbstractAnnotationConfigDispatcherServletInitializer是Spring对WebApplicationInitializer接口的基础实现 * 在Servlet3.0环境中,容器会在类路径中查找实现了ServletContainerInitializer接口的类,如果能发现的话,就用它来配置Servlet容器 * Spring提供了这个接口的实现,名为SpringServletContainerInitializer,而这个实现类会查找WebApplicationInitializer来完成配置任务 * 也就是说,我们只要扩展AbstractAnnotationConfigDispatcherServletInitializer类,就可以配置Servlet应用上下文和Dispatcher-Servlet */ public class SpittrWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { // 指定根上下文配置类 @Override protected Class<?>[] getRootConfigClasses() { return new Class<?>[] { RootConfig.class }; } // 指定Spring应用上下文配置类,Spring应用上下文会位于应用程序的Servlet上下文之中 @Override protected Class<?>[] getServletConfigClasses() { return new Class<?>[] { WebConfig.class }; } // 将DispatcherServlet映射到 "/" @Override protected String[] getServletMappings() { return new String[] { "/" }; } }
-
这里我们详细解释一下DispatcherServlet 和 ContextLoaderListener
DispatcherServlet (前端控制器)
-
DispatcherServlet
会在应用服务器启动时被加载,作为前端控制器 (Front Controller) 来处理所有的 HTTP 请求。 -
请求处理流程:
- 接受请求:
DispatcherServlet
拦截所有进入的 HTTP 请求。 - 请求映射:根据请求 URL,通过
HandlerMapping
找到相应的处理器 (Controller
)。 - 调用处理器:调用相应的
Controller
方法处理请求。 - 返回视图:处理器返回
ModelAndView
对象,DispatcherServlet
使用ViewResolver
解析视图并返回响应结果。
- 接受请求:
-
作用:
- 充当整个 Spring MVC 请求处理流程的中央控制器。
- 负责将请求分发给相应的控制器,并根据控制器的返回值选择合适的视图进行展示。
- 管理 Spring MVC 中的各种组件(如
HandlerMapping
、ViewResolver
、HandlerAdapter
、ExceptionResolver
等)。
-
加载 Web 应用上下文 (WebApplicationContext),包含与 MVC Web 层相关的 Bean(如控制器、视图解析器、拦截器等)。
ContextLoaderListener (上下文加载监听器)
ContextLoaderListener
用于 启动和管理 Spring 应用的根上下文 (ApplicationContext),通常用于加载应用的基础配置文件(如 Bean 配置、数据源配置、事务管理等)。- 作用:
- 加载 根应用上下文 (Root ApplicationContext),即整个应用的基础 Bean 配置,如数据库连接池、业务服务、DAO 层、事务管理器等。
ContextLoaderListener
初始化的上下文在应用程序的整个生命周期中是共享的,因此适合存放与 Web 层无关的业务逻辑和数据访问层 Bean。- 保证在
DispatcherServlet
之前加载,以便DispatcherServlet
可以访问由根上下文加载的 Bean。
根上下文 (Root ApplicationContext)
-
由
ContextLoaderListener
初始化,通常在 Web 应用启动时加载。 -
根上下文的配置文件通常命名为
applicationContext.xml
或通过@Configuration
注解定义。 -
职责:主要加载和管理与 Web 层无关的 Bean,例如:
- 数据源 (DataSource)
- 业务服务 (Service)
- 数据访问对象 (DAO)
- 事务管理器 (Transaction Manager)
- 各种工具类、配置属性等
-
作用范围:整个 Web 应用程序的生命周期,因此所有的
DispatcherServlet
都可以访问根上下文中的 Bean。 -
特点:根上下文的 Bean 可以被所有 Web 应用上下文(
DispatcherServlet
)访问,但 Web 应用上下文中的 Bean 不能被根上下文访问。
Web 应用上下文 (WebApplicationContext)
-
由
DispatcherServlet
初始化,每个DispatcherServlet
实例可以有一个独立的 Web 应用上下文。 -
Web 应用上下文的配置文件通常命名为
dispatcher-servlet.xml
,也可以通过注解配置。 -
职责:加载和管理与 Web 层相关的 Bean,例如:
- 控制器 (Controllers)
- 视图解析器 (ViewResolver)
- 拦截器 (Interceptors)
- 处理器映射 (HandlerMapping)
-
作用范围:仅限于某个
DispatcherServlet
,因此每个DispatcherServlet
可以有自己的 Web 应用上下文,独立于其他DispatcherServlet
。 -
特点:Web 应用上下文会继承根上下文中的 Bean,因此可以访问根上下文中的 Bean,但根上下文无法访问 Web 应用上下文中的 Bean。
AbstractAnnotationConfigDispatcherServletInitializer的作用
-
实际上,
AbstractAnnotationConfigDispatcherServletInitializer
会同时创建DispatcherServlet
和ContextLoaderListener
。- **getServletConfigClasses()**方法返回的带有@Configuration注解的类将会用来配置Web应用上下文中的Bean
- **getRootConfigClasses()**方法返回的带有@Configuration注解的类将会用来配置根上下文中的Bean
启用Spring MVC
-
最简单的Spring MVC配置如下
-
JAVA
package spittr.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.EnableWebMvc; /** * */ @Configuration @EnableWebMvc public class WebConfig { }
-
-
这可以启动Spring MVC,但是至少有三个问题需要解决
- 没有配置视图解析器,如果这样,Spring默认会使用BeanNameViewResolver,这个视图解析器会查找ID与视图名称匹配的Bean,并且查找的Bean要实现View接口。
- 没有启用组件扫描,Spring只能找到显式声明在配置类中的控制器
- DispatcherServlet会映射为应用的默认Servlet,因此他会处理所有的请求,包括对静态资源的请求,如图片和样式表(在大多数情况下,这并不是你想要的效果)
-
我们可以给上述最简单的WebConfig添加一些内容,以解决这些问题
-
JAVA
package spittr.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.ViewResolver; import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.view.InternalResourceViewResolver; /** * 书中使用的是继承WebMvcConfigurerAdapter类,但是这个类在Spring 5.xx 或 Spring Boot 2.xx 版本中已弃用 * 这里使用的是平替的方法,直接实现WebMvcConfigurer接口即可。 */ @Configuration @EnableWebMvc @ComponentScan("spittr.web") public class WebConfig implements WebMvcConfigurer { // 配置JSP视图解析器 @Bean public ViewResolver viewResolver() { InternalResourceViewResolver resolver = new InternalResourceViewResolver(); // 这里指定的意思是会去查找WEB-INF/views/xxx.jsp的文件作为视图,呈现给用户 // 注意不要写成/WEB-INF/views 这样查找的就是WEB-INF/viewsxxx.jsp,就是404 not found resolver.setPrefix("/WEB-INF/views/"); resolver.setSuffix(".jsp"); resolver.setExposeContextBeansAsAttributes(true); return resolver; } // 配置静态资源的处理 @Override public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { configurer.enable(); } }
- 我们添加了一个InternalResourceViewResolver,在这里,我们只需知道他会查找jsp文件,在查找时,他会在视图名称上添加一个特定的前缀和后缀,例如:名为home的视图将会解析为/WEB-INF/views/home.jsp
- 我们还实现了configureDefaultServletHandling方法,通过调用DefaultServletHandlerConfigurer 的 enable()方法,要求DispatcherServlet将对静态资源的请求转发到Servlet容器默认的Servlet上,而不是使用DispatcherServlet本身来处理此类请求
-
-
RootConfig暂且配置如下:
package spittr.config; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.FilterType; import org.springframework.web.servlet.config.annotation.EnableWebMvc; /** * */ @Configuration // basePackages = {"spittr"}:这个属性指定了组件扫描的基础包,这里是spittr包 // excludeFilters:这个属性用于指定在组件扫描过程中需要排除的特定类或注解。它可以基于注解、类、类型等进行过滤。这里排除所有标注了@EnableWebMvc注解的类 @ComponentScan(basePackages = {"spittr"}, excludeFilters = { @ComponentScan.Filter(type= FilterType.ANNOTATION, value= EnableWebMvc.class) }) public class RootConfig { }
编写我们的第一个Controller
-
假设我们要编写一个控制器,来处理对"/"的请求,并渲染应用的首页。
-
这很简单,具体如下:
package spittr.web.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import javax.annotation.PostConstruct; import static org.springframework.web.bind.annotation.RequestMethod.GET; /** * */ // @Controller其实用途和@Component差不多,只是用@Controller能更好的表达它的用途 @Controller public class HomeController { // 这里,@RequestMapping的value属性指定了这个方法所要处理的请求路径,method方法指定了它所处理的HTTP方法 // 即,当收到对"/"的HTTP GET请求时,就会调用home()方法 @RequestMapping(value = "/" , method=GET) public String home() { // 视图名为home,DispatcherServlet会要求视图解析器将这个逻辑名称解析为实际的视图 // 由于配置InternalResourceViewResolver的方式,视图名"home"将会解析为"/WEB-INF/views/home.jsp"路径的jsp // System.out.println("homeController active"); return "home"; } // 这个注解使得这个方法在依赖注入完成后,对象被创建并初始化完成后立即执行,这里是为了保证spring应用被顺利的部署到tomcat服务器上 @PostConstruct public void init() { System.out.println("Spring application context initialized successfully!"); } }
-
这里home.jsp具体如下
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ page session="false" %> <html> <head> <title>Spittr</title> <%-- 静态资源应该放在static目录中,并且应该使用相对路径去访问他们,这里style.css位于 resource/static/style.css 中--%> <%-- 可以在浏览器中使用 http://localhost:8080/static/style.css 路径去访问它--%> <link rel="stylesheet" type="text/css" href="<c:url value="/static/style.css" />" > </head> <body> <h1>Welcome to Spittr</h1> <a href="<c:url value=" /spittles" />">Spittles</a> <a href="<c:url value=" /spitter/register" />">Register</a> </body> </html>
注意事项(如果想要使用Spring + Tomcat来查看执行的结果)
-
这里格外需要注意的是项目的结构,我将项目结构贴在这里,配合tomcat就能够将项目给顺利的启动起来
. ├── pom.xml └── src ├── main │ ├── java │ │ └── spittr │ │ ├── config │ │ │ ├── RootConfig.java │ │ │ ├── SpittrWebAppInitializer.java │ │ │ └── WebConfig.java │ │ └── web │ │ └── controller │ │ └── HomeController.java │ ├── resources │ │ └── static │ │ └── style.css │ └── webapp │ ├── WEB-INF │ │ └── views │ │ └── home.jsp │ └── web.xml └── test └── java
-
并且需要注意的是tomcat10.x.x及以上的版本使用的包名为jakarta.servlet.*
-
而tomcat9.x.x及以下的版本使用的包名为javax.servlet.*
-
并且如果想要使用tomcat10.x.x及以上的版本,必须配合Spring6.x或更高版本,因为Spring6.x或更高版本使用jakarta.servlet包,并且Spring6.x版本要求Java17及以上版本,也就是说,如果你想使用Tomcat + Spring的组合开发,则只能使用Java 17及以上版本开发,如果使用tomcat9.x.x及以下的版本,则必须配合Spring5.x或以下版本,因为Spring5.x及以下版本仍然使用的是javax.servlet包
-
如果想要使用jsp,在tomcat10.x.x及以上版本,至少需要在maven中导入如下所示的包
<!--版本推荐至少需要大于这里展示的版本--> <dependency> <groupId>jakarta.servlet</groupId> <artifactId>jakarta.servlet-api</artifactId> <version>6.0.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>jakarta.servlet.jsp</groupId> <artifactId>jakarta.servlet.jsp-api</artifactId> <version>3.0.0</version> <scope>provided</scope> </dependency>
-
如果需要使用jstl,还需要引入这样的包(tomcat10.x.x及以上),具体可以查阅如下网址:jsp - How to install JSTL? It fails with “The absolute uri cannot be resolved” or “Unable to find taglib” or NoClassDefFoundError or ClassCastException - Stack Overflow
-
如果不导入这几个包,会报错未能加载或实例化TagLibraryValidator类
<dependency> <groupId>jakarta.servlet.jsp.jstl</groupId> <artifactId>jakarta.servlet.jsp.jstl-api</artifactId> <version>3.0.0</version> </dependency> <dependency> <groupId>org.apache.taglibs</groupId> <artifactId>taglibs-standard-impl</artifactId> <version>1.2.5</version> </dependency> <dependency> <groupId>org.apache.taglibs</groupId> <artifactId>taglibs-standard-spec</artifactId> <version>1.2.5</version> </dependency> <dependency> <groupId>org.glassfish.web</groupId> <artifactId>jakarta.servlet.jsp.jstl</artifactId> <version>3.0.1</version> </dependency>
-
如果上述内容都配置好了,就只需在tomcat中配置好工件就可以访问http://localhost:8080来查看我们的界面了!
测试这个Controller
-
首先,我们正如我们之前所说,@Controller注解其实本质上和@Component的功能是一样的,也就是说HomeController其实只是一个POJO(简单JAVA对象)因此,测试它其实很简单。
import org.junit.Test; import spittr.web.controller.HomeController; import static org.junit.Assert.assertEquals; /** * */ public class HomeControllerTest { @Test public void testHomePage() throws Exception { HomeController controller = new HomeController(); assertEquals("home",controller.home()); } }
-
但正如我们所说,它只测试了home()方法中会发生什么,他并没有从MVC控制器的的视角进行测试,不过下面这个重写后的例子则很好的弥补了这一点。
import org.junit.Test; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import spittr.web.controller.HomeController; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view; /** * */ public class HomeControllerTest { @Test public void testHomePage() throws Exception { HomeController controller = new HomeController(); // 搭建MockMvc // MockMvc是Spring提供的一个用于模拟 HTTP 请求并验证控制器响应的测试工具。它帮助我们在测试环境中模拟一个 HTTP 请求,并验证该请求的结果,而不需要启动整个Spring容器。 // standaloneSetup 方法用于创建一个只包含指定控制器的测试环境,而不是启动整个Spring上下文。这个方法是为了进行单元测试时的轻量化操作,它不依赖于真实的Web环境。 MockMvc mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); // 对"/"执行GET请求,预期得到home视图 mockMvc.perform(get("/")).andExpect(view().name("home")); } }
- 我们发起了对"/"的GET请求,并断言结果视图的名称为home。这样更加完整了测试了Controller的职责。
传递模型数据到视图中
-
这一节笔者采用和原书不同的结构去构建,最终呈现的结果与我们要学习的知识是相同的
-
首先先展示项目结构(在本节完成时你应当与我有类似的项目结构)
. ├── pom.xml └── src ├── main │ ├── java │ │ └── spittr │ │ ├── config │ │ │ ├── RootConfig.java │ │ │ ├── SpittrWebAppInitializer.java │ │ │ └── WebConfig.java │ │ └── web │ │ ├── controller │ │ │ ├── HomeController.java │ │ │ └── SpittleController.java │ │ ├── dao │ │ │ └── SpittleDao.java │ │ ├── entity │ │ │ └── Spittle.java │ │ └── service │ │ ├── ISpittleService.java │ │ └── impl │ │ └── SpittleServiceImpl.java │ ├── resources │ │ ├── logback-test.xml │ │ └── static │ │ └── css │ │ └── style.css │ └── webapp │ ├── WEB-INF │ │ └── views │ │ ├── home.jsp │ │ └── spittles.jsp │ └── web.xml └── test └── java └── HomeControllerTest.java
-
我们先来设计Spittle这个POJO类,具体如下所示
package spittr.web.entity; import lombok.Getter; import lombok.Setter; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import java.util.Date; /** * */ @Getter @Setter public class Spittle { private final Long id; private final String message; private final Date time; //经纬度 private Double latitude; private Double longitude; public Spittle(String message,Date time) { this(message,time,null,null); } public Spittle(String message, Date time, Double longitude, Double latitude) { this.id = null; this.message = message; this.time = time; this.longitude = longitude; this.latitude = latitude; } @Override public boolean equals(Object that) { return EqualsBuilder.reflectionEquals(this, that, "id", "time"); } @Override public int hashCode() { return HashCodeBuilder.reflectionHashCode(this, "id", "time"); } }
-
需要注意的有两个点
-
第一,是类上面使用的@Getter和@Setter注解,这两个注解的使用可以让我们不必手动的去实现所有的Getter和Setter方法,而依赖第三方库来通过反射机制自动生成这些方法,减少代码量。
-
但要想使用这两个注解,需要在maven中导入如下所示的依赖
<!--为了使用@Getter,@Setter,@Data,@Slf4j等注解,简化代码--> <!--这里Slf4j注解能够自动的在你的代码中声明一个log对象,而不必手动声明,可以直接使用log.info之类的方法记录日志--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.30</version> <scope>provided</scope> </dependency>
-
如果想要使用@Slf4j注解, 请根据如下方法配置,这里提供简单的logback的配置教程:
-
首先在maven中导入相关库:
<!-- SLF4J API --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>2.0.0</version> </dependency> <!--logback依赖--> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.4.14</version> <!--下面这个开启就只有在test文件夹中的才会显示日志信息--> <!--<scope>test</scope>--> </dependency>
-
然后在resources目录下新建一个logback-test.xml,具体内容如下:
<?xml version="1.0" encoding="UTF-8"?> <configuration debug="false"> <!-- 控制台输出 --> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符--> <!--<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>--> <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern> </encoder> </appender> <!-- 日志输出级别,logback日志级别包括五个:TRACE < DEBUG < INFO < WARN < ERROR --> <root level="DEBUG"> <appender-ref ref="STDOUT"/> <!--<appender-ref ref="FILE"/>--> </root> </configuration>
-
这样就可以轻松愉快的使用日志了!通过这么配置,我们还能清晰的看到每个访问所对应的控制器,便于我们发现问题。
-
第二,是代码中使用的EqualsBuilder和HashCodeBuilder,如果想要使用这两个工具类,需要在maven中导入如下依赖:
<!--为了使用EqualsBuilder与HashCodeBuilder工具类--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.12.0</version> </dependency>
-
导入了这个库之后就不必手动实现繁琐的equals()和hashCode()方法了,直接使用上面两个工具类即可。
-
-
-
随后我们来设计ISpittleService,提供一个很简单的功能,返回一些博文(Spittles)给前端视图中
-
具体如下:
package spittr.web.service; import spittr.web.entity.Spittle; import java.util.List; /** * */ public interface ISpittleService { // max代表返回的Spittle中Spittle ID属性的最大值,而count参数表明要返回多少个Spittle对象 List<Spittle> findSpittles(long max, int count); }
-
实现该接口。借助数据访问层(dao)来完成实际功能,随后将会给出SpittleDao的实现。
package spittr.web.service.impl; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import spittr.web.dao.SpittleDao; import spittr.web.entity.Spittle; import spittr.web.service.ISpittleService; import java.util.List; /** * */ @Slf4j @Service public class SpittleServiceImpl implements ISpittleService { private final SpittleDao spittleDao; @Autowired public SpittleServiceImpl(SpittleDao spittleDao) { this.spittleDao = spittleDao; } @Override public List<Spittle> findSpittles(long max, int count) { // 这里随便查找一些数据给前端,简单的进行测试 return spittleDao.query_all_spittle(); } }
-
dao层先不进行数据库的复杂配置,而是简单的假装实现这个功能(以后连接数据库时会更新这一部分)
package spittr.web.dao; import org.springframework.stereotype.Repository; import spittr.web.entity.Spittle; import java.util.ArrayList; import java.util.Date; import java.util.List; /** * */ @Slf4j @Repository public class SpittleDao { public List<Spittle> query_all_spittle() { List<Spittle> results = new ArrayList<>(); Spittle spittle1 = new Spittle("Hello World! The First ever spittle!", new Date(), 0.0, 0.0); Spittle spittle2 = new Spittle("Here's another spittle", new Date(), 128.0, 256.0); Spittle spittle3 = new Spittle("Spittle spittle spittle", new Date(), 0.0, 0.0); Spittle spittle4 = new Spittle("Spittles go fourth!", new Date(), 0.0, 0.0); results.add(spittle1); results.add(spittle2); results.add(spittle3); results.add(spittle4); return results; } }
-
现在,我们控制器来调用服务层的方法,给用户显示最近的博客信息
package spittr.web.controller; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import spittr.web.service.ISpittleService; import static org.springframework.web.bind.annotation.RequestMethod.GET; /** * */ @Slf4j @Controller @RequestMapping("/spittles") public class SpittleController { private final ISpittleService spittleService; @Autowired public SpittleController(ISpittleService spittleService) { this.spittleService = spittleService; } @RequestMapping(method=GET) public String spittles(Model model) { model.addAttribute("spittleList", spittleService.findSpittles(0,0)); // 返回视图名,同上,spring会去寻找WEB-INF/views/spittles.jsp return "spittles"; } }
-
这里是本节的核心,也就是如何将模型数据传送到视图中
-
这里首先应当注意到的是Model类,Model是 Spring MVC 框架中的一个接口,用于在控制器(Controller)中传递数据到视图(View)。它主要用于处理控制器方法与视图之间的数据交互。
-
它实际上是一个Map(即key-value对的集合),键是数据的名字,值是数据的内容。控制器可以通过调用 addAttribute 方法将数据添加到模型中,视图可以通过使用对应的键来获取到数据的内容并使用这些数据渲染页面。
-
它有两个常用的方法,第一个是这里的addAttribute,通过这个方法可以将数据存储到模型中
-
还有一个asMap()方法,这个方法常用于从模型中获取到所有的数据
Map<String, Object> map = model.asMap();
-
但并非一定要使用Model这个Spring中的特定类型,直接使用Map也是可以的,相同功能的源码如下:
@RequestMapping(method=GET) public String spittles(Map model) { model.put("spittleList", spittleService.findSpittles(0,0)); return "spittles"; }
-
实际上,你甚至不一定需要显式的设置模型和返回视图名称
-
像是下面这样的代码也是可以正常运行的
@RequestMapping(method=GET) public List<Spittle> spittles() { return spittleRepository.findSpittles(0,0); }
-
下面来解释一下为什么这样的代码也能够正确的运行
-
第一:为什么未显式的设置模型但是前端能够拿到数据?
Spring中,当处理器方法像现在这样返回对象或集合时,这个值会放到模型中,模型的key会根据当前类型推断得出(在本例中,也就是spittleList)
-
第二:为什么未显式的指定逻辑视图的名称但是Spring知道应该在哪里查找视图?
Spring中,逻辑视图的名称可以通过请求路径推断得出,因为这个方法处理针对"/spittles"的GET请求,因此视图的名称将会被推断为spittles
-
-
-
好了,最后,数据已经放在了模型中,在JSP中该如何去访问它呢?
-
实际上,当视图是JSP时,模型数据将会作为请求属性放在请求(request)之中,因此,可以使用JSTL中的<c:forEach>来渲染博客列表
-
具体如下:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Spittles</title> <link rel="stylesheet" type="text/css" href="<c:url value="/static/css/style.css" />" > </head> <body> <c:forEach items="${spittleList}" var="spittle" > <li id="spittle_<c:out value="spittle.id"/>"> <div class="spittleMessage"> <c:out value="${spittle.message}"/> </div> <div> <span class="spittleTime"><c:out value="${spittle.time}"/></span> <span class="spittleLocation"> (<c:out value="${spittle.latitude}"/>, <c:out value="${spittle.longitude}"/>) </span> </div> </li> </c:forEach> </body> </html>
-
-
最后,让我们来编写一个单元测试,以确保它的功能正常
import lombok.extern.slf4j.Slf4j; import org.junit.Test; import org.mockito.Mock; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.servlet.view.InternalResourceView; import spittr.web.controller.SpittleController; import spittr.web.dao.SpittleDao; import spittr.web.entity.Spittle; import spittr.web.service.impl.SpittleServiceImpl; import java.util.ArrayList; import java.util.Date; import java.util.List; import static org.hamcrest.CoreMatchers.hasItems; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; /** * */ @Slf4j public class SpittleControllerTest { @Test public void shouldShowRecentSpittles() throws Exception { List<Spittle> expectedSpittles = createSpittleList(); // 配置mock对象,这里使用了mockito库,需要在maven中添加依赖 SpittleDao mockDao = mock(SpittleDao.class); // 设置mock对象在调用query_all_spittle()方法后的反应 when(mockDao.query_all_spittle()).thenReturn(expectedSpittles); SpittleServiceImpl spittleService = new SpittleServiceImpl(mockDao); SpittleController controller = new SpittleController(spittleService); // 配置mockMvc MockMvc mockMvc = MockMvcBuilders.standaloneSetup(controller).setSingleView(new InternalResourceView("/WEB-INF/views/spittles.jsp")).build(); // 访问/spittles,判断行为是否符合预期 mockMvc.perform(get("/spittles")).andExpect(view().name("spittles")).andExpect(model().attributeExists("spittleList")).andExpect(model().attribute("spittleList", hasItems(expectedSpittles.toArray()))); } private List<Spittle> createSpittleList() { List<Spittle> results = new ArrayList<>(); Spittle spittle1 = new Spittle("Hello World! The First ever spittle!", new Date(), 0.0, 0.0); Spittle spittle2 = new Spittle("Here's another spittle", new Date(), 128.0, 256.0); Spittle spittle3 = new Spittle("Spittle spittle spittle", new Date(), 0.0, 0.0); Spittle spittle4 = new Spittle("Spittles go fourth!", new Date(), 0.0, 0.0); results.add(spittle1); results.add(spittle2); results.add(spittle3); results.add(spittle4); return results; } }
-
这里要使用mock方法,需要在maven中导入如下的库(Java 17版本可用),其他版本请自行测试是否可用
<!--mock依赖--> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>4.11.0</version> <scope>test</scope> </dependency>
-
接受带有用户输入的请求
- SpringMVC允许以多种方式将客户端中的数据传送到控制器的处理器方法中,包括:
- 查询参数
- 表单参数
- 路径变量
查询参数
-
java
private static final String MAX_LONG_AS_STRING = "9223372036854775807"; /* 这个控制器方法将可以处理前端传递max,count变量给后端时应该做的行为,具体的说: 对于get("/spittles?max=238900&count=50")或者get("/spittles")这种请求就会调用该方法 value指前端传递的信息的名称,defauleValue指未指定该变量的值时的默认值 默认返回spittles.jsp作为视图 */ @RequestMapping(method=GET) public List<Spittle> spittles ( @RequestParam(value = "max", defaultValue = MAX_LONG_AS_STRING) long max, @RequestParam(value = "count", defaultValue = "20") int count) { return spittleService.findSpittles(max,count); }
路径参数
-
java
/* 这个控制器方法将会处理形如/show/12这样的Get请求,用来展示不同id的文章非常方便 当用户访问/show/123时,123将会作为spittleId的值传递给后端的控制器方法,后端就可以通过这个id去查找相应的文章给用户进行展示 */ @RequestMapping(value="/show/{spittleId}", method=GET) public List<Spittle> spittle( @PathVariable("spittleId") long spittleId, Model model){ model.addAttribute("theSpittle", spittleService.findOne(spittleId)); return "spittle"; }
处理表单
-
jsp表单类似这样
<form method="POST"> First Name:<input type="text" name="firstName"/><br/> LastName:<input type="text" name="lastName"/><br/> Username:<input type="text" name="username"/><br/> Password:<input type="password" name="password"/><br/> <input type="submit" value="Register"/> </form>
-
与之对应的控制器方法如下
@PostMapping(value="/register") public String processRegistration(Spitter spitter){ spitterRepository.save(spitter); return "redirect:/spitter/"+ spitter.getUsername(); }
-
这里有两点需要说明:
-
第一,Spring MVC 通过数据绑定机制将表单中的表单项自动组装成一个类对象。具体来说,Spring 会根据表单中的
name
属性与目标类(如Spitter
)的属性名进行匹配,并将表单数据填充到类的实例中。-
例如,表单提交的形式可能如下
firstName=John&lastName=Doe&username=johndoe&password=123456
-
Spring MVC 会根据目标类(如
Spitter
)的属性名,自动将表单数据绑定到类的实例中。 -
例如,表单中的
firstName
会匹配Spitter
类中的firstName
属性,lastName
会匹配lastName
属性,以此类推。 -
Spring 会调用目标类的 setter 方法(如
setFirstName
、setLastName
等)来设置属性值。 -
如果
Spitter
类只有部分属性在表单中存在- Spring 只会填充表单中存在的、且与
Spitter
类属性名匹配的属性。表单中不存在或是不匹配的属性值将会保持默认值(如null或初始值)
- Spring 只会填充表单中存在的、且与
-
如果表单中有
Spitter
类中不存在的字段- 如果表单中有
Spitter
类中不存在的字段,Spring 会忽略这些字段,不会报错。
- 如果表单中有
-
如果表单字段与类属性不完全匹配时,例如表单中为username,而类中为userName时
- 使用@RequestParam(“username”) 去单独获取到
-
-
第二,你可能注意到这次返回的视图名被加上了redirect:前缀。
- 实际上,Spring的
InternalResourceViewResolver
可以识别redirect:
前缀和forward:
前缀 - Forward(转发):
- 转发是服务器端的操作,服务器将请求从一个资源(如 Servlet 或 JSP)传递到另一个资源,客户端(浏览器)并不知道这个过程。
- 浏览器只发起一次请求,URL 不会改变。即客户端无感知。
- 转发的目标资源可以访问原始请求中的所有数据(如请求参数、属性等)。即共享请求数据。
- Redirect(重定向):
- 重定向是客户端的操作,服务器告诉客户端(浏览器)去请求一个新的资源,浏览器会发起一个新的请求。
- 客户端发起新请求:浏览器会发起两次请求,第一次请求服务器,服务器返回一个重定向响应(状态码 302),浏览器根据响应中的新 URL 发起第二次请求。
- URL 改变:浏览器的地址栏会显示新的 URL。
- 不共享请求数据:重定向后,原始请求中的数据(如请求参数、属性等)不会自动传递给新的请求,除非显式地将数据附加到新的 URL 中。
- **常用于:**避免表单重复提交,需要跳转到外部资源时,需要改变浏览器的url时。
- 实际上,Spring的
-
Java校验API
-
空值校验
@NotNull
校验字段值不能为null
,但可以为空字符串或空集合。@Null
校验字段值必须为null
。@NotEmpty
校验字段值不能为null
或空(适用于字符串、集合、数组等)。@NotBlank
校验字符串字段值不能为null
,且必须包含至少一个非空白字符。
-
范围校验
@Min
校验数字字段的最小值。@Max
校验数字字段的最大值。@DecimalMin
校验数字字段的最小值(支持字符串形式的数字)。@DecimalMax
校验数字字段的最大值(支持字符串形式的数字)。@Size
校验字段的长度或大小(适用于字符串、集合、数组等)。
-
正则校验
@Pattern
校验字符串字段是否匹配指定的正则表达式。
-
日期校验
@Past
校验日期字段必须是过去的日期。@PastOrPresent
校验日期字段必须是过去或当前的日期。@Future
校验日期字段必须是未来的日期。@FutureOrPresent
校验日期字段必须是未来或当前的日期。
-
其他校验
@Email
校验字符串字段必须是合法的电子邮件地址。@Positive
校验数字字段必须是正数。@Negative
校验数字字段必须是负数。
-
使用:
- 首先在需要校验的字段上添加校验注解,建议加上message字段,以便告知用户出错原因
- 多个校验注解可以叠加使用
@NotNull private String name; @Null private String unusedField; @NotEmpty private List<String> items; @NotBlank(message = "请输入用户名") private String username; @Min(18) private int age; @Max(100) private int score; @DecimalMin("0.01") private BigDecimal price; @DecimalMax("100.00") private BigDecimal discount; @Size(min = 2, max = 10) private String password; @Pattern(regexp = "^[a-zA-Z0-9]{6,12}$") private String username; @Past private LocalDate birthDate; @Email private String email; @Positive private int quantity;
-
因为我们是Spring项目,不需要引用什么依赖了,可以直接使用@Valid注解,来表示我们对这个对象的属性需要验证
@Controller public class TestController { @PostMapping("/test") public String testVaild(@Valid theClass theclass, BindingResult result) { // 验证所有字段是否验证通过 if(result.hasErrors()) { // 当字段中存在不合法的情况时,返回第一条错误信息给前端 return result.getAllErrors().get(0).getDefaultMessage(); } return "success"; } }