本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:Java EE(Java Platform, Enterprise Edition)是用于构建分布式、多层企业级应用的标准平台,其核心优势之一在于通过注解(Annotations)简化配置、提升代码可读性与可维护性。本项目围绕Java EE中的关键注解和程序开发技术展开,涵盖从Web组件定义、依赖注入、数据持久化到事务管理的全流程实践。内容包括@WebServlet、@EJB、@Entity、@Inject等常用注解的实际应用,结合Servlet、JSP、EJB、JPA、JMS、RESTful服务及WebSocket等核心技术,帮助开发者掌握现代企业级Java应用的构建方法。通过本课程设计,学习者将能够使用注解替代传统XML配置,实现高效、低耦合的企业系统开发。

Java EE平台的企业级应用架构与注解驱动开发实践

在当今的分布式系统时代,企业级应用早已不再是简单的CRUD操作堆砌。面对高并发、高可用、易维护等多重挑战,Java EE(现在称为Jakarta EE)依然扮演着不可替代的角色——它不仅是一套技术规范,更是一种成熟的工程化解决方案。想象一下:你正在为一家银行构建核心交易系统,每秒要处理数千笔请求,任何一次数据库连接泄漏或事务配置错误都可能导致灾难性后果。这时,你会感激Java EE提供的标准化容器管理机制和声明式编程模型。

而这一切的核心驱动力之一,就是 注解(Annotations) 。从最初被视为“语法糖”的元数据标记,到如今成为现代Java企业开发的基石,注解已经彻底改变了我们编写代码的方式。它让原本分散在XML文件中的配置信息回归代码本身,实现了真正的“约定优于配置”。更重要的是,结合CDI、EJB、JPA等规范,注解使得依赖注入、事务控制、安全权限等功能变得轻而易举。

接下来,我们将深入探索这个由注解编织而成的企业级架构世界。准备好了吗?让我们开始这场旅程吧!🚀


注解的力量:不只是元数据那么简单

很多人第一次接触Java注解时,可能会觉得这不过是个“带名字的标签”,比如 @Override @Deprecated 。但当你真正进入Java EE的世界后,你会发现这些小小的 @ 符号背后隐藏着惊人的能力。

为什么是注解?告别XML地狱的时代

还记得那个被 web.xml 支配的恐惧吗?一个中等规模的应用可能就有几百行XML配置,分布在十几个文件里。修改一个Servlet路径?好,先找到对应的 <servlet-mapping> ;调整初始化参数?再去翻一遍 <init-param> ……这种割裂感简直让人抓狂 😵‍💫。

<!-- 想想这段熟悉的噩梦 -->
<servlet>
    <servlet-name>ReportServlet</servlet-name>
    <servlet-class>com.corp.reporting.ReportServlet</servlet-class>
    <init-param>
        <param-name>format</param-name>
        <param-value>PDF</param-value>
    </init-param>
</servlet>
<servlet-mapping>
    <servlet-name>ReportServlet</servlet-Name>
    <url-pattern>/reports/*</url-pattern>
</servlet-mapping>

而有了注解之后呢?

@WebServlet(
    urlPatterns = "/reports/*",
    initParams = @WebInitParam(name = "format", value = "PDF")
)
public class ReportServlet extends HttpServlet { ... }

看到了吗? 同一个组件的所有相关信息现在都集中在一起了 。IDE可以轻松导航、重构、查找引用,编译器还能帮你检查拼写错误。这才是真正的开发者友好设计!

💡 小知识: @WebServlet 是 Servlet 3.0 引入的特性。在此之前,所有Servlet都必须通过 web.xml 配置。这一变革标志着Java EE正式迈入“零XML”时代。

元注解:打造你的专属注解语言

Java提供了四个关键的 元注解 (meta-annotations),它们的作用就像是“注解的注解”,用来定义其他注解的行为规则:

元注解 作用
@Target 这个注解能用在哪里?类?方法?字段?
@Retention 它保留到什么时候?源码?字节码?还是运行时?
@Documented 是否应该出现在Javadoc文档中?
@Inherited 子类能否自动继承父类上的这个注解?

让我们动手写一个自定义注解来感受一下:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface Auditable {
    String action() default "UNKNOWN";
    boolean logParameters() default false;
}

这个 @Auditable 注解表示某个方法需要被审计记录。它的设计意图非常明确:
- 只能标注在方法上 ✅
- 必须保留到运行期以便反射读取 ✅
- 应该出现在API文档中 ✅
- 如果父类方法被标记,子类重写时也应继承此行为 ✅

然后你就可以这样使用它:

public abstract class BaseService {
    @Auditable(action = "CREATE_USER", logParameters = true)
    public abstract void createUser(User user);
}

@Service
public class UserServiceImpl extends BaseService {
    @Override
    public void createUser(User user) {
        // 实现逻辑
    }
}

注意!这里并没有重复写 @Auditable ,但由于父类已声明且该注解带有 @Inherited ,所以子类自然继承了审计能力。这是不是有点像面向切面编程(AOP)的味道了?😎

⚠️ 一个小陷阱:接口不会继承注解!

虽然 @Inherited 对类继承有效,但它 对实现接口无效 。也就是说,即使你在接口方法上加了注解,实现类也不会自动拥有它。这是Java语言的一个设计限制,很多开发者初次遇到都会踩坑。

public interface PaymentService {
    @Auditable(action = "PAY")
    void pay(Order order);
}

@Service
public class AlipayService implements PaymentService {
    @Override
    public void pay(Order order) {
        // ❌ 不会触发审计!因为接口上的注解不被继承
    }
}

解决办法有两个:
1. 在实现类的方法上手动加上 @Auditable
2. 使用Spring AOP这类更强的代理机制进行全局拦截

动手实现一个性能监控处理器

光说不练假把式。我们来做一个真实的例子:用注解 + 动态代理实现一个非侵入式的性能监控系统。

第一步:定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PerformanceLogged {
    String value() default ""; // 日志前缀
    boolean includeStackTrace() default false;
}
第二步:编写动态代理处理器
public class PerformanceLoggingHandler implements InvocationHandler {
    private final Object target;

    public PerformanceLoggingHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        PerformanceLogged perfLog = method.getAnnotation(PerformanceLogged.class);

        if (perfLog == null) {
            return method.invoke(target, args); // 无注解则直接调用
        }

        long start = System.currentTimeMillis();
        String label = !perfLog.value().isEmpty() ? 
                       perfLog.value() : method.getName();

        System.out.printf("[⏱️] 开始执行: %s.%s%n", 
                          target.getClass().getSimpleName(), label);

        try {
            Object result = method.invoke(target, args);
            long elapsed = System.currentTimeMillis() - start;

            System.out.printf("[✅] 执行完成 '%s',耗时: %d ms%n", label, elapsed);
            return result;

        } catch (InvocationTargetException e) {
            long elapsed = System.currentTimeMillis() - start;
            System.err.printf("[❌] 方法失败 '%s',耗时: %d ms%n", label, elapsed);

            if (perfLog.includeStackTrace()) {
                e.printStackTrace();
            }
            throw e.getCause(); // 保持原始异常类型
        }
    }

    @SuppressWarnings("unchecked")
    public static <T> T createProxy(T instance) {
        Class<?> clazz = instance.getClass();
        return (T) Proxy.newProxyInstance(
            clazz.getClassLoader(),
            clazz.getInterfaces(),
            new PerformanceLoggingHandler(instance)
        );
    }
}
第三步:实际应用测试
@Service
public interface OrderService {
    void processOrder(String orderId);
}

@Service
public class OrderServiceImpl implements OrderService {

    @PerformanceLogged(value = "订单主流程", includeStackTrace = true)
    @Override
    public void processOrder(String orderId) {
        try {
            Thread.sleep(500); // 模拟业务处理
        } catch (InterruptedException ignored) {}
        System.out.println("✅ 订单 [" + orderId + "] 处理完毕");
    }
}

// 测试入口
public class TestApp {
    public static void main(String[] args) {
        OrderService service = PerformanceLoggingHandler.createProxy(new OrderServiceImpl());
        service.processOrder("ORD-20240405-001");
    }
}

输出结果如下:

[⏱️] 开始执行: OrderServiceImpl.订单主流程
✅ 订单 [ORD-20240405-001] 处理完毕
[✅] 执行完成 '订单主流程',耗时: 502 ms

瞧见没?我们没有改动任何业务代码逻辑,只是通过注解+代理的方式,就给方法加上了性能追踪功能。这就是所谓的 横切关注点分离

🎯 真实场景提示:在生产环境中,你应该将日志输出改为SLF4J,并集成Micrometer或OpenTelemetry用于指标上报,而不是简单打印到控制台。


Web层革命:用 @WebServlet 和 @WebFilter 构建现代化前端控制器

如果说EJB是Java EE的“老贵族”,那么基于注解的Servlet和Filter就是新时代的“平民英雄”。它们以极低的学习成本和强大的灵活性,支撑起了无数Web应用的骨架。

@WebServlet:告别 web.xml 的自由之路

自从 Servlet 3.0 起, @WebServlet 成为了注册Servlet的新标准方式。它的基本用法很简单:

@WebServlet("/api/hello")
public class HelloServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
            throws IOException {
        resp.getWriter().write("Hello from annotation-driven world!");
    }
}

但这只是冰山一角。真正厉害的是它的高级特性:

多路径映射 & 初始化参数
@WebServlet(
    urlPatterns = {
        "/user/profile",
        "/api/v1/user",
        "/rest/user-info"
    },
    name = "UserProfileServlet",
    initParams = {
        @WebInitParam(name = "cacheTimeout", value = "3600"),
        @WebInitParam(name = "defaultAvatar", value = "/images/default.png")
    },
    loadOnStartup = 1,
    asyncSupported = true
)
public class UserProfileServlet extends HttpServlet {
    private int cacheTimeout;
    private String defaultAvatar;

    @Override
    public void init() throws ServletException {
        cacheTimeout = Integer.parseInt(getInitParameter("cacheTimeout"));
        defaultAvatar = getInitParameter("defaultAvatar");
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        // 使用初始化参数
        resp.setHeader("Cache-Control", "max-age=" + cacheTimeout);
        // ...
    }
}

几个关键点你要记住:
- loadOnStartup=1 表示优先加载,适合缓存预热类组件;
- asyncSupported=true 是开启异步处理的前提;
- initParams 相当于以前的 <init-param> ,但更直观;
- 支持多个URL绑定到同一个Servlet,非常适合API版本兼容。

异步处理实战:别再阻塞线程池!

传统的同步Servlet在一个请求到来时会占用一个容器线程直到结束。如果这个请求涉及远程调用或复杂计算,线程就会白白等待,极大浪费资源。

解决方案?异步IO!

@WebServlet(urlPatterns = "/async/task", asyncSupported = true)
public class AsyncTaskServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        AsyncContext asyncCtx = req.startAsync();
        asyncCtx.setTimeout(30_000); // 30秒超时

        // 提交到独立线程池处理,释放容器线程
        CompletableFuture.runAsync(() -> {
            try {
                Thread.sleep(10_000); // 模拟耗时任务
                ServletResponse response = asyncCtx.getResponse();
                response.getWriter().write("Task completed asynchronously.");
                asyncCtx.complete();
            } catch (Exception e) {
                try {
                    asyncCtx.getResponse().sendError(500, e.getMessage());
                } finally {
                    asyncCtx.complete();
                }
            }
        }, ForkJoinPool.commonPool());
    }
}
对比项 同步处理 异步处理
线程占用 请求期间全程占用 仅短暂占用,立即释放
并发能力 受限于线程池大小(通常几百) 可达数万级别
适用场景 快速响应操作 文件上传、第三方API调用、长轮询

🔥 性能建议:对于需要长时间等待外部服务响应的接口,强烈推荐使用异步模式。你可以配合 CompletableFuture + 线程池实现更精细的资源控制。

REST风格接口也能这么简单?

尽管有JAX-RS(如Jersey),但在某些轻量级场景下,原生Servlet依然是最佳选择。比如嵌入式Tomcat、微服务网关、内部工具系统等。

下面是一个完整的RESTful用户管理示例:

@WebServlet("/api/users/*")
public class RestUserServlet extends HttpServlet {
    private final Map<String, User> users = new ConcurrentHashMap<>();
    private final ObjectMapper json = new ObjectMapper();

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
            throws IOException {
        String pathInfo = req.getPathInfo();
        resp.setContentType("application/json");

        if (pathInfo == null || "/".equals(pathInfo)) {
            // GET /api/users → 列出所有
            resp.getWriter().print(json.writeValueAsString(users.values()));
        } else {
            // GET /api/users/{id} → 查询单个
            String id = pathInfo.substring(1);
            User user = users.get(id);
            if (user != null) {
                resp.getWriter().print(json.writeValueAsString(user));
            } else {
                resp.setStatus(SC_NOT_FOUND);
                resp.getWriter().print("{\"error\":\"User not found\"}");
            }
        }
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) 
            throws IOException {
        req.setCharacterEncoding("UTF-8");
        User user = json.readValue(req.getReader(), User.class);
        users.put(user.getId(), user);

        resp.setStatus(SC_CREATED);
        resp.getWriter().print("{\"status\":\"created\",\"id\":\"" + user.getId() + "\"}");
    }

    @Override
    protected void doDelete(HttpServletRequest req, HttpServletResponse resp) 
            throws IOException {
        String id = req.getPathInfo().substring(1);
        boolean removed = users.remove(id) != null;
        resp.setStatus(removed ? SC_NO_CONTENT : SC_NOT_FOUND);
    }
}

这个例子展示了几个重要技巧:
- 使用 getPathInfo() 提取REST路径参数;
- 借助Jackson处理JSON序列化/反序列化;
- 选用 ConcurrentHashMap 保证线程安全;
- 正确设置HTTP状态码表达语义;
- 所有写操作均返回明确的结果反馈。

graph TD
    A[HTTP Request] --> B{Method?}
    B -->|GET| C{Path Info?}
    C -->|/api/users| D[List All Users]
    C -->|/api/users/{id}| E[Find By ID]
    B -->|POST| F[Parse JSON & Create User]
    B -->|DELETE| G[Remove User by ID]
    D --> H[Return JSON Array]
    E --> I[Return Single JSON or 404]
    F --> J[Save to Map, Return 201]
    G --> K[Return 204 or 404]

当然,这只是一个演示原型。在真实项目中你还应该加入:
- 输入校验(JSR-303 Bean Validation)
- 异常统一处理(@WebFilter 拦截并格式化错误)
- 分页支持(添加 page , size 参数解析)
- 安全过滤(防止XSS、SQL注入)

@WebFilter:横切关注点的最佳实践场所

如果说Servlet是“点”,那Filter就是“线”——它贯穿整个请求生命周期,是实现通用逻辑的理想位置。

统一字符编码过滤器(再也不怕中文乱码!)

这是一个经典问题:浏览器发送的中文参数到了后台变成乱码。原因通常是编码不一致导致的。解决方法就是强制统一编码。

@WebFilter(filterName = "EncodingFilter", urlPatterns = "/*", asyncSupported = true)
public class EncodingFilter implements Filter {
    private String encoding = "UTF-8";

    @Override
    public void init(FilterConfig config) throws ServletException {
        String param = config.getInitParameter("encoding");
        if (param != null && !param.trim().isEmpty()) {
            encoding = param;
        }
    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        // POST请求编码设置
        if ("POST".equalsIgnoreCase(request.getMethod())) {
            request.setCharacterEncoding(encoding);
        }

        // 响应编码设置
        response.setContentType("text/html;charset=" + encoding);
        response.setCharacterEncoding(encoding);

        // 包装请求对象以解决GET参数乱码(Tomcat默认ISO-8859-1)
        chain.doFilter(new ReadableRequestWrapper(request, encoding), response);
    }
}

class ReadableRequestWrapper extends HttpServletRequestWrapper {
    private final String encoding;

    public ReadableRequestWrapper(HttpServletRequest request, String encoding) {
        super(request);
        this.encoding = encoding;
    }

    @Override
    public String getParameter(String name) {
        String value = super.getParameter(name);
        if (value == null || value.isEmpty()) return value;

        // Tomcat默认按ISO-8859-1解码,需重新转码
        try {
            return new String(value.getBytes("ISO-8859-1"), encoding);
        } catch (UnsupportedEncodingException e) {
            return value; // fallback
        }
    }
}

⚠️ 注意事项:现代应用服务器(如Tomcat 8+)通常已在connector层面支持UTF-8,因此你可能不需要手动做GET参数转码。但了解原理仍然很重要!

日志记录过滤器:谁动了我的接口?

想知道每个请求是谁发起的、花了多久、走了哪些路径?日志过滤器来帮你!

@Priority(100) // 最早执行
@WebFilter(filterName = "RequestLogFilter", urlPatterns = "/*")
public class RequestLogFilter implements Filter {

    private static final Logger LOG = LoggerFactory.getLogger(RequestLogFilter.class);

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
        String clientIP = getClientIp(request);
        String uri = request.getRequestURI();
        long startTime = System.currentTimeMillis();

        LOG.info("👉 {} {} from {}", request.getMethod(), uri, clientIP);

        try {
            chain.doFilter(req, res);
            int status = response.getStatus();
            long duration = System.currentTimeMillis() - startTime;
            LOG.info("✅ {} {} {} in {}ms", request.getMethod(), uri, status, duration);
        } catch (Exception e) {
            long duration = System.currentTimeMillis() - startTime;
            LOG.error("💥 {} {} failed after {}ms: {}", 
                      request.getMethod(), uri, duration, e.getMessage());
            throw e;
        }
    }

    private String getClientIp(HttpServletRequest request) {
        String xff = request.getHeader("X-Forwarded-For");
        if (xff != null && !xff.isEmpty() && !"unknown".equalsIgnoreCase(xff)) {
            return xff.split(",")[0].trim();
        }
        return request.getRemoteAddr();
    }
}

这里用了 @Priority(100) 来确保它是第一个被执行的过滤器。如果你还有认证、压缩等其他Filter,它们会在日志之后依次执行。


依赖注入的艺术:@Inject vs @Resource

终于来到了Java EE最迷人的部分之一: 依赖注入(DI) 。它不仅是技术手段,更是一种思维方式的转变——从“我主动去找资源”变为“资源自动送上门”。

控制反转(IoC)到底解决了什么问题?

假设你要做个订单系统,其中 OrderService 需要用到支付网关:

public class OrderService {
    private PaymentGateway gateway = new AlipayGateway(); // 紧耦合!

    public void placeOrder(Order order) {
        gateway.pay(order.getAmount());
    }
}

看起来没问题?错!问题大了去了:
- 换成微信支付怎么办?改源码?
- 单元测试怎么模拟支付失败?没法mock!
- 多个环境用不同实现?得靠条件判断?

正确做法是: 面向接口编程 + 由容器注入实现

public interface PaymentGateway {
    void pay(BigDecimal amount);
}

@ApplicationScoped
public class AlipayGateway implements PaymentGateway { ... }

@RequestScoped
public class OrderService {
    @Inject
    private PaymentGateway paymentGateway; // 容器自动注入

    public void placeOrder(Order order) {
        paymentGateway.pay(order.getAmount());
    }
}

现在你想换微信支付?只需换个实现类,甚至可以通过配置切换。想测试?写个 MockPaymentGateway 注入进去就行。一切都在运行时决定,代码完全解耦!

CDI:Java EE的依赖注入心脏

CDI(Contexts and Dependency Injection)是 Java EE 6 引入的核心规范(JSR-299)。它统一了之前EJB、JSF各自的注入机制,成为整个平台的事实标准。

要启用CDI,你只需要在 src/main/resources/META-INF/ 下放一个 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">
</beans>

其中 bean-discovery-mode 决定了扫描策略:
- all :扫描所有类(最方便,适合新项目)
- annotated :只扫描带CDI注解的类(启动更快)
- none :关闭自动发现(完全手动控制)

一旦激活,CDI容器就开始工作了。它会:
1. 扫描类路径下的候选Bean;
2. 根据作用域创建实例;
3. 解析依赖关系图;
4. 在适当时候注入字段或调用方法。

作用域管理:对象生命周期的精确控制

CDI支持多种内置作用域,让你精准掌控对象何时创建、何时销毁:

作用域 注解 生命周期
应用级 @ApplicationScoped 整个应用生命周期内唯一实例
会话级 @SessionScoped 用户会话期间存在(HTTP Session)
请求级 @RequestScoped 每次HTTP请求新建一个
对话级 @ConversationScoped 跨多个请求的长期交互(如向导流程)

例如:

@ApplicationScoped
public class DatabasePool {
    private DataSource dataSource;

    @PostConstruct
    void init() {
        // 初始化连接池
    }
}

@RequestScoped
public class RequestContext {
    private String traceId;
    private Instant startTime;

    @PostConstruct
    void begin() {
        traceId = UUID.randomUUID().toString();
        startTime = Instant.now();
    }
}

前者作为单例共享,后者每次请求都会新建,避免状态污染。

消除歧义:当有多个候选Bean时怎么办?

最常见的问题是: 接口有多个实现类,容器不知道该注入哪一个

public interface UserRepository { ... }

@ApplicationScoped
public class JpaUserRepository implements UserRepository { ... }

@ApplicationScoped
public class MongoUserRepository implements UserRepository { ... }

@RequestScoped
public class UserService {
    @Inject
    private UserRepository repo; // ❌ 部署时抛出DeploymentException!
}

解决方法是使用 限定符(Qualifier)

@Qualifier
@Retention(RUNTIME)
@Target({FIELD, METHOD, PARAMETER})
public @interface Primary {}

@Primary
@ApplicationScoped
public class JpaUserRepository implements UserRepository { ... }

@RequestScoped
public class UserService {
    @Inject @Primary
    private UserRepository repo; // ✅ 明确指定使用主实现
}

你也可以预定义一些通用限定符,比如:

@Qualifier
@Retention(RUNTIME)
@Target({FIELD, METHOD, PARAMETER})
public @interface ReadOnly {}

@Qualifier
@Retention(RUNTIME)
@Target({FIELD, METHOD, PARAMETER})
public @interface ForReporting {}

然后这样使用:

@Inject @ReadOnly
private UserRepository queryRepo;

@Inject @ForReporting
private AnalyticsService analytics;

是不是瞬间清晰多了?🎯

@Resource:专用于容器托管资源的注入

除了通用的 @Inject ,Java EE还提供了 @Resource 用于注入特定类型的基础设施资源,比如:

  • 数据源(DataSource)
  • JMS连接工厂
  • 定时任务调度器
  • 邮件会话(Mail Session)
@Stateless
public class OrderProcessor {

    @Resource(lookup = "java:jboss/datasources/ExampleDS")
    private DataSource dataSource;

    @Resource
    private TimerService timerService;

    @Resource(mappedName = "java:/jms/queue/OrderQueue")
    private Queue orderQueue;
}

它的特点是基于 名称查找 而非类型匹配。如果省略 lookup ,容器会根据字段名推断JNDI路径,例如:
- dataSource java:comp/env/dataSource
- mailSession java:comp/env/mailSession

这在集成传统JNDI资源时特别有用。

@EJB 和 @PersistenceContext:领域专用注入利器

还有一些更专业的注入方式值得了解:

注入EJB组件
@Stateless
public class InventoryService { ... }

@Stateless
public class OrderService {
    @EJB
    private InventoryService inventory; // 注入本地EJB
}

@EJB 支持更多属性控制,比如:

@EJB(
    beanName = "InventoryService",
    beanInterface = InventoryService.class,
    lookup = "global/myapp/InventoryService"
)
private InventoryService inventory;
注入JPA EntityManager
@Stateless
public class CustomerDAO {
    @PersistenceContext(unitName = "customerPU")
    private EntityManager em;

    public Customer findById(Long id) {
        return em.find(Customer.class, id);
    }
}

这里的 unitName 必须与 persistence.xml 中定义的一致:

<persistence-unit name="customerPU">
    <jta-data-source>java:jboss/datasources/ExampleDS</jta-data-source>
    <class>com.example.Customer</class>
</persistence-unit>

📦 小贴士:如果你使用Spring Boot,默认采用 LocalContainerEntityManagerFactoryBean ,此时 @PersistenceContext 依然可用,但底层实现略有不同。


项目结构实战:搭建一个可维护的企业级应用

纸上谈兵终觉浅。现在我们来亲手搭建一个标准的Java EE项目骨架。

Maven项目结构推荐

my-enterprise-app/
├── pom.xml
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── com/example/
│   │   │       ├── web/              # Servlet、Filter
│   │   │       ├── service/          # EJB或CDI Service
│   │   │       ├── repository/       # DAO层
│   │   │       ├── entity/           # JPA实体
│   │   │       └── config/           # 自定义配置类
│   │   ├── resources/
│   │   │   ├── META-INF/
│   │   │   │   └── persistence.xml
│   │   │   └── beans.xml             # CDI启用标志
│   │   └── webapp/
│   │       ├── WEB-INF/
│   │       │   └── web.xml           # 可选,用于全局配置
│   │       └── index.html
│   └── test/
│       └── java/
│           └── com/example/test/

核心POM依赖配置

<properties>
    <maven.compiler.source>11</maven.compiler.source>
    <maven.compiler.target>11</maven.compiler.target>
    <failOnMissingWebXml>false</failOnMissingWebXml>
</properties>

<dependencies>
    <!-- Java EE Web Profile API -->
    <dependency>
        <groupId>javax</groupId>
        <artifactId>javaee-web-api</artifactId>
        <version>8.0.1</version>
        <scope>provided</scope>
    </dependency>

    <!-- Testing -->
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>5.9.0</version>
        <scope>test</scope>
    </dependency>

    <!-- Optional: Lombok for boilerplate reduction -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.30</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

WAR vs EAR:如何选择打包方式?

类型 特点 适用场景
WAR 单一模块,包含Web资源和Java类 微服务、前后端一体应用
EAR 多模块归档,可包含多个WAR/JAR 大型企业系统,模块化部署

使用Maven构建EAR非常简单:

<packaging>ear</packaging>
<modules>
    <module>web-module.war</module>
    <module>ejb-module.jar</module>
</modules>
<plugins>
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-ear-plugin</artifactId>
        <version>3.4.0</version>
    </plugin>
</plugins>

部署与监控:让系统跑起来并看得见

最后一步,把你的应用部署到服务器上。

WildFly部署命令

# 连接到管理接口并部署
jboss-cli.sh --connect --command="deploy /path/to/app.war"

# 查看部署状态
/subsystem=deployment-scanner/scanner=primary:read-resource

如何排查常见问题?

1. 组件未生效?
  • 检查是否缺少 beans.xml
  • 查看服务器日志是否有扫描错误
  • 确认类路径正确( WEB-INF/classes 或 JAR)
  • 使用IDE查看“Deployment Structure”视图
2. 依赖注入失败?
  • 检查Bean是否被正确识别( @ApplicationScoped 等)
  • 是否存在歧义(多个实现类)
  • 作用域是否兼容(不能将在请求作用域中注入单例)
3. 性能瓶颈在哪?
  • 开启慢查询日志定位N+1问题
  • 使用APM工具(New Relic、SkyWalking)分析调用链
  • 监控GC频率与内存使用情况

🛠️ 推荐工具组合:Prometheus + Grafana + Micrometer,实时可视化JVM指标与业务数据。


结语:Java EE从未过时,只是换了种方式存在

尽管Spring生态如日中天,但我们不应忘记Java EE(Jakarta EE)所带来的深远影响。它所倡导的标准化、容器化、声明式编程思想,早已融入现代Java开发的血液之中。

今天你写的每一个 @Component @Autowired @Transactional ,其实都能在Java EE中找到原型。只不过Spring做了更好的抽象和推广。

所以,无论你是继续深耕传统Java EE,还是转向Spring Boot,掌握这套底层原理都将让你在技术道路上走得更稳、更远。毕竟,真正的高手,从来不拘泥于框架,而是理解其背后的哲学 💡。

愿你在代码的世界里,永远保持好奇与热爱 ❤️!

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:Java EE(Java Platform, Enterprise Edition)是用于构建分布式、多层企业级应用的标准平台,其核心优势之一在于通过注解(Annotations)简化配置、提升代码可读性与可维护性。本项目围绕Java EE中的关键注解和程序开发技术展开,涵盖从Web组件定义、依赖注入、数据持久化到事务管理的全流程实践。内容包括@WebServlet、@EJB、@Entity、@Inject等常用注解的实际应用,结合Servlet、JSP、EJB、JPA、JMS、RESTful服务及WebSocket等核心技术,帮助开发者掌握现代企业级Java应用的构建方法。通过本课程设计,学习者将能够使用注解替代传统XML配置,实现高效、低耦合的企业系统开发。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐