JavaEE6中引入了CDI规范,CDI(Contexts and Dependency Injection)提供了一种类型安全的依赖注入机制,它能够帮助开发者通过管理Bean的上下文和依赖关系来构建高内聚、低耦合的应用。
CDI规范的诞生其实是“借(抄)鉴(袭)”了Spring框架的成功经验。JavaEE6及之后的版本中,JSF托管Bean、EJB等也是CDI管理的,此外Servlet虽然不是CDI托管Bean但Servlet中也可以使用CDI注解进行注入,不仅如此,我们还可以将任何我们想要的Java类定义为CDI的托管Bean,这些托管Bean可以互相依赖注入,这极大的提高了我们程序的可扩展性。
使用CDI需要引入JavaEE SDK,我们这里使用JavaEE8版本,CDI的具体实现由JavaEE应用服务器提供,我们这里选择Wildfly14应用服务器。注意Tomcat不支持CDI,这个规范需要标准JavaEE应用服务器。
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<version>8.0.1</version>
<scope>provided</scope>
</dependency>
如果你想要在JavaSE中使用CDI其实也是可以的,只不过你需要额外引入CDI的实现,例如JBoss Weld,然而这并不是一个常见的做法,因为非JavaEE下Spring是更好的选择。
JavaEE工程使用CDI时,通常还需要一个beans.xml
配置文件,它可以放置在Web工程的WEB-INF
或其它类型工程的META-INF
目录下。
<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/beans_2_0.xsd"
version="2.0" bean-discovery-mode="all">
</beans>
配置中,bean-discovery-mode="all"
表示开启CDI注解扫描。
注意:beans.xml
并不是强制性必须添加的,bean-discovery-mode
的默认值就是all
,因此即使我们不添加beans.xml
,CDI注解依然可以工作。然而CDI中我们还可能用到拦截器、装饰器等全局性的配置,此时还是需要配置在这个beans.xml
里,所以即使没有什么特殊配置我们也最好在项目里添加一个类似上面的空beans.xml
,以免给队友造成迷惑。
CDI的托管Bean有若干种生命周期,它们可以通过注解标注。
@ApplicationScoped:应用全局单例
@SessionScoped:用户会话生命周期
@RequestScoped:HTTP请求生命周期
@Dependent:默认,随注入对象生命周期
下面例子代码中我们创建了一个CDI托管的Bean,DemoService
,它标注了@ApplicationScoped
注解,是全局单例的。
package com.gacfox.netstore.web.service;
import org.jboss.logging.Logger;
import javax.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class DemoService {
private static final Logger logger = Logger.getLogger(DemoService.class);
public void sayHello() {
logger.info("Hello, CDI!");
}
}
在Servlet中,我们使用@Inject
注解注入了这个CDI Bean并调用了sayHello()
方法。
package com.gacfox.netstore.web.servlet;
import com.gacfox.netstore.web.service.DemoService;
import javax.inject.Inject;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet(name = "DemoServlet", urlPatterns = "/demo")
public class DemoServlet extends HttpServlet {
@Inject
private DemoService demoService;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
demoService.sayHello();
}
}
CDI中,Qualifiers(限定符)用于帮助区分具有相同类型的不同Bean。当多个Bean实现了相同的接口时,容器无法决定注入哪一个Bean,这时候我们可以使用限定符来明确指定注入的Bean。@Named
是一个常用的内置限定符,它允许你为Bean指定一个名称,这个名称可以用来在注入时进行匹配。
package com.gacfox.netstore.web.service;
public interface DemoBean {
void sayHello();
}
package com.gacfox.netstore.web.service;
import org.jboss.logging.Logger;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Named;
@Named("ABean")
@ApplicationScoped
public class ABean implements DemoBean{
private static final Logger logger = Logger.getLogger(ABean.class);
@Override
public void sayHello() {
logger.info("Hello, I'm ABean!");
}
}
package com.gacfox.netstore.web.service;
import org.jboss.logging.Logger;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Named;
@Named("BBean")
@ApplicationScoped
public class BBean implements DemoBean{
private static final Logger logger = Logger.getLogger(BBean.class);
@Override
public void sayHello() {
logger.info("Hello, I'm BBean!");
}
}
package com.gacfox.netstore.web.servlet;
import com.gacfox.netstore.web.service.DemoBean;
import javax.inject.Inject;
import javax.inject.Named;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet(name = "DemoServlet", urlPatterns = "/demo")
public class DemoServlet extends HttpServlet {
@Inject
@Named("ABean")
private DemoBean demoBean;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
demoBean.sayHello();
}
}
上面例子代码中,ABean
和BBean
都实现了DemoBean
接口,但我们为其设置了限定符@Named("ABean")
,因此ABean
会正确注入到Servlet中并被调用。
除了使用注解声明式的创建CDI Bean,我们也可以用代码定义CDI Bean的创建方法,这被称为Bean生产者方法。下面例子代码中,public FooBean fooBean()
就是这样的一个Bean生产者,它标注了@Produces
注解。
package com.gacfox.netstore.web.service;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Produces;
import javax.inject.Named;
@ApplicationScoped
public class BeanProducer {
@Produces
@Named("customFooBean")
public FooBean fooBean() {
return new FooBean();
}
}
在Servlet中,我们可以使用@Inject
注入Bean生产者生产的Bean。
package com.gacfox.netstore.web.servlet;
import com.gacfox.netstore.web.service.FooBean;
import javax.inject.Inject;
import javax.inject.Named;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet(name = "DemoServlet", urlPatterns = "/demo")
public class DemoServlet extends HttpServlet {
@Inject
@Named("customFooBean")
private FooBean fooBean;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
fooBean.sayHello();
}
}
CDI支持Bean拦截器实现AOP切面,下面例子我们定义了一个日志切面,在Bean的执行前后打印日志。CDI中创建Bean拦截器需要一个Binding注解,此外拦截器类需要标注@Interceptor
和对应的Binding注解,并创建一个@AroundInvoke
方法,方法的参数InvocationContext
就是被拦截的方法的执行上下文,我们可以在context.proceed()
被调用前后插入切面逻辑。
package com.gacfox.netstore.web.service;
import javax.interceptor.InterceptorBinding;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@InterceptorBinding
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Loggable {
}
package com.gacfox.netstore.web.service;
import org.jboss.logging.Logger;
import javax.interceptor.AroundInvoke;
import javax.interceptor.Interceptor;
import javax.interceptor.InvocationContext;
@Interceptor
@Loggable
public class LoggableInterceptor {
private static final Logger logger = Logger.getLogger(LoggableInterceptor.class);
@AroundInvoke
public Object logMethodEntry(InvocationContext context) throws Exception {
logger.infov("Before method: {0}", context.getMethod().getName());
Object result = context.proceed();
logger.infov("After method: {0}", context.getMethod().getName());
return result;
}
}
对于被拦截的Bean,我们可以使用@Interceptors
指定注解类,此外这个Bean也需要标注Binding注解。
package com.gacfox.netstore.web.service;
import org.jboss.logging.Logger;
import javax.enterprise.context.ApplicationScoped;
import javax.interceptor.Interceptors;
@ApplicationScoped
@Loggable
@Interceptors(LoggableInterceptor.class)
public class DemoService {
private static final Logger logger = Logger.getLogger(DemoService.class);
public void sayHello() {
logger.info("Hello, CDI!");
}
}
如果我们需要全局配置拦截器,可以在beans.xml
中进行配置,此时被拦截的Bean只需要标注Binding注解即可,不需要再标注@Interceptors
了。
<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/beans_2_0.xsd"
version="2.0" bean-discovery-mode="all">
<interceptors>
<class>com.gacfox.netstore.web.service.LoggableInterceptor</class>
</interceptors>
</beans>
装饰器模式是一种用于增强或修改函数行为的设计模式。CDI中,与拦截器(Interceptor)类似,装饰器(Decorator)也允许你在不修改原始Bean代码的情况下添加额外的逻辑。然而,装饰器主要用于扩展或修改特定Bean的行为,装饰器是明确类型感知的,即它知道被装饰的Bean的具体类型,因此可以针对特定类型进行定制;而拦截器虽然也能感知类型,但它的API设计其实倾向于为多种Bean统一添加切面逻辑,而不是特定扩展某一个Bean。
带装饰器的Bean必须采用接口加实现类的方式编写,下面是一个例子。
package com.gacfox.netstore.web.service;
public interface TargetService {
void sayHello();
}
package com.gacfox.netstore.web.service;
import org.jboss.logging.Logger;
import javax.enterprise.context.RequestScoped;
@RequestScoped
public class TargetServiceImpl implements TargetService {
private static final Logger logger = Logger.getLogger(TargetServiceImpl.class);
@Override
public void sayHello() {
logger.info("Hello, I'm TargetService!");
}
}
装饰器类也必须实现和被装饰Bean相同的接口,此外还需要标注@Decorator
注解。装饰器类内也需要注入被装饰的Bean,字段上还需要标注@Delegate
,这样我们就可以在目标方法被执行时调用被装饰的对象,实现装饰器扩展逻辑了。
package com.gacfox.netstore.web.service;
import org.jboss.logging.Logger;
import javax.decorator.Decorator;
import javax.decorator.Delegate;
import javax.inject.Inject;
@Decorator
public class TargetServiceDecorator implements TargetService {
private static final Logger logger = Logger.getLogger(TargetServiceDecorator.class);
@Inject
@Delegate
private TargetService targetService;
@Override
public void sayHello() {
logger.info("Before method");
targetService.sayHello();
logger.info("Before method");
}
}
在beans.xml
中,我们需要配置前面编写的装饰器类。
<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/beans_2_0.xsd"
version="2.0" bean-discovery-mode="all">
<decorators>
<class>com.gacfox.netstore.web.service.TargetServiceDecorator</class>
</decorators>
</beans>
此时调用被装饰的Bean,我们会发现装饰器的扩展逻辑也执行了。