依赖注入实战:构建 Twitter 克隆应用
在软件开发中,依赖注入(Dependency Injection,DI)是一种强大的设计模式,它能够提高代码的可维护性、可测试性和可扩展性。本文将通过构建一个名为 Crosstalk 的 Twitter 克隆应用,详细介绍如何在实际项目中应用依赖注入。
1. Crosstalk 应用概述
Crosstalk 是一个简单的微博应用,允许用户在个人主页上发布短消息(即 tweets)。它具有以下特点:
-
可扩展性
:能够轻松应对大量用户同时访问。
-
高并发处理
:确保多个用户可以同时进行操作而不会出现性能问题。
-
模块化设计
:将不同功能模块分离,降低耦合度。
Crosstalk 的主要需求包括:
-
用户认证
:在用户登录时进行身份验证。
-
页面安全
:保护用户主页的安全。
-
数据持久化
:将用户发布的 tweets 存储到数据库中。
为了实现这些需求,我们将使用以下技术栈:
-
Google Guice
:作为依赖注入框架,负责管理对象之间的依赖关系。
-
Google Sitebricks
:作为 Web 应用框架,用于渲染网页。
-
Hibernate
:作为持久化框架,实现数据的存储和读取。
-
Hypersonic(HSQL)
:作为内存数据库,方便开发和测试。
2. 应用设置
在开始构建 Crosstalk 应用之前,我们需要进行一些基础设置。
2.1 项目结构
项目的基本结构如下:
-
src
:主源代码目录,包含所有 Java 类。
-
test
:测试源代码目录,用于存放 Jetty 启动器。
-
web
:Web 资源目录,包含 HTML 模板和 CSS 样式表。
-
web/WEB-INF
:Servlet 容器所需的部署描述符目录。
2.2 添加依赖库
我们需要添加一系列依赖库到项目的类路径中,具体如下表所示:
| 库名称 | 描述 |
|---|---|
| guice-2.0.jar | Google Guice 核心库 |
| aopalliance.jar | Guice AOP 接口 |
| google-sitebricks.jar | Google Sitebricks Web 框架 |
| guice-servlet-2.0.jar | Guice 的 Servlet 集成库 |
| hibernate3.jar | Hibernate 核心持久化框架 |
| hibernate-annotations.jar | Hibernate 注解库 |
| ejb3-persistence.jar | Hibernate 注解库 |
| dom4j-1.6.1.jar | Dom4j XML 解析器 |
| jta.jar | Java 事务 API |
| cglib-nodep-2.1_3.jar | Hibernate 用于代理对象的 CGLib 库 |
| antlr-2.7.5h3.jar | Antlr 编译器库 |
| commons-collections.jar | Apache 集合库 |
| commons-logging.jar | Apache Commons-Logging 库 |
| warp-persist-2.0-20090214.jar | 用于集成 Hibernate 和 Guice 的库 |
| hsqldb.jar | Hypersonic 内存 SQL 数据库 |
| servlet-api-2.5-6.1.9.jar | Java Servlet API |
| jetty-6.1.9.jar | Mort Bay Jetty |
| jetty-util-6.1.9.jar | Jetty 工具库 |
| jcip-annotations.jar | 线程安全注解库 |
在 IntelliJ IDEA 中添加这些库到类路径的步骤如下:
1. 打开 Settings > Project Settings。
2. 选择主(crosstalk)模块。
3. 打开 Dependencies 标签。
4. 点击 Add。
5. 选择 Project Library 并点击 Add Jar Directory,选择所有 jar 文件所在的目录(或者使用单条目模块库选项逐个添加)。
如果不使用 IDE,可以将所有 jar 文件放在一个 lib 目录中,并在命令行中分别指定它们。
2.3 配置注入器
应用的核心配置位于
CrosstalkBootstrap
类中,它是
GuiceServletContextListener
的子类。以下是该类的代码:
public final class CrosstalkBootstrap extends GuiceServletContextListener {
@Override
protected Injector getInjector() {
// 绑定所有服务依赖
final Module services = new ServicesModule();
// 告诉 Sitebricks 扫描这个包
final Module sitebricks = new SitebricksModule() {
protected void configureSitebricks() {
scan(CrosstalkBootstrap.class.getPackage());
}
};
// 将所有传入请求通过 PersistenceFilter 进行过滤
final Module servlets = new ServletModule() {
protected void configureServlets() {
filter("/*").through(PersistenceFilter.class);
install(sitebricks);
}
};
// 最后,使用所有配置创建注入器
return Guice.createInjector(services, servlets);
}
}
这个类的主要作用是创建并注册我们自己的注入器,同时告诉 Guice 如何将服务连接在一起。
2.4 配置 web.xml
在
web.xml
文件中,我们需要注册
GuiceFilter
和
CrosstalkBootstrap
监听器,代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<web-app 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"
version="2.5">
<filter>
<filter-name>guiceFilter</filter-name>
<filter-class>com.google.inject.servlet.GuiceFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>guiceFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<listener>
<listener-class>com.wideplay.crosstalk.CrosstalkBootstrap</listener-class>
</listener>
</web-app>
通过这个配置,所有传入的请求都会经过
GuiceFilter
,从而允许我们利用依赖注入的特性处理请求。
3. 配置 Google Sitebricks
Google Sitebricks 是一个简单的静态类型开发系统,用于渲染网页。我们需要对其进行一些额外的配置。
3.1 路由请求到 Google Sitebricks
在
CrosstalkBootstrap
类中,我们已经将所有请求通过
PersistenceFilter
进行过滤,并安装了
SitebricksModule
,代码如下:
final Module servlets = new ServletModule() {
protected void configureServlets() {
filter("/*").through(PersistenceFilter.class);
install(sitebricks);
}
};
这使得 Google Sitebricks 可以处理所有用户请求,并决定哪些请求需要处理,哪些请求可以直接传递给 Servlet 容器。
3.2 配置扫描包
我们还需要告诉 Google Sitebricks 扫描哪些包,代码如下:
final Module sitebricks = new SitebricksModule() {
@Override
protected void configureSitebricks() {
scan(CrosstalkBootstrap.class.getPackage());
}
};
通过提供
CrosstalkBootstrap
的包名,我们让 Google Sitebricks 扫描以
com.wideplay.crosstalk
开头的整个包树,查找需要服务的页面类和模板。
4. Crosstalk 的模块化和服务耦合
为了提高代码的可维护性和可扩展性,Crosstalk 采用了模块化设计。
4.1 包结构
Crosstalk 的包结构简单明了,主要分为以下几个部分:
-
com.wideplay.crosstalk.web
:表示层,包含所有页面类。
-
com.wideplay.crosstalk.services
:持久化和安全层,只暴露接口,实现类作为包私有隐藏起来。
-
com.wideplay.crosstalk.tweets
:领域模型包,包含数据模型类。
这种结构的好处是降低了模块之间的耦合度,我们可以在不影响其他模块的情况下修改具体的实现细节。例如,如果我们想将数据存储方式从 Hibernate 和 HSQL 改为集群数据存储,只需要修改
services
包中的实现类即可。
4.2 服务耦合
模块之间的协作通过公开暴露的接口和明确定义的契约进行,避免了意外的服务耦合。这种设计使得代码更加灵活和易于维护。
5. 表示层
Crosstalk 的核心功能是允许用户在个人主页上发布 tweets,因此表示层的设计至关重要。
5.1 HomePage 类
我们创建了一个名为
HomePage
的类来处理用户主页的逻辑,代码如下:
package com.wideplay.crosstalk.web;
@At("/home") @Select("action") @RequestScoped
public class HomePage {
// 用户上下文,跟踪当前用户
private User user;
// 页面状态变量
private List<Tweet> tweets;
private Tweet newTweet = new Tweet();
// 服务依赖
private final TweetManager tweetManager;
@Inject
public HomePage(TweetManager tweetManager, User user) {
this.tweetManager = tweetManager;
this.user = user;
}
@Get("logout")
public String logout() {
user.logout();
return "/login?message=Bye.";
}
@Get
public String get() {
// 加载当前用户的 tweets
this.tweets = tweetManager.tweetsFor(user.getUsername());
// 留在当前页面
return null;
}
@Post
public String post() {
newTweet.setAuthor(user.getUsername());
// 将新 tweet 添加到数据存储中
tweetManager.addTweet(newTweet);
// 使用 GET 请求重定向回此页面
return "/home";
}
// getters/setters...
public String getUser() {
return user.getUsername();
}
public List<Tweet> getTweets() {
return tweets;
}
public Tweet getNewTweet() {
return newTweet;
}
}
该类使用了一些注解来实现不同的功能:
-
@At("/home")
:告诉 Google Sitebricks 在访问
/home
路径时提供该页面。
-
@RequestScoped
:告诉 Guice 为每个传入请求创建一个新的
HomePage
实例,确保用户数据的独立性。
-
@Get
和
@Post
:分别处理 GET 和 POST 请求。
5.2 HomePage 模板
HomePage
的 HTML 模板是一个简单的文件,它通过绑定数据到
HomePage
类的请求作用域实例,实现了动态文本的显示。以下是模板代码:
<html>
<head>
<title>Tweets</title>
<link rel="stylesheet" href="/crosstalk.css"/>
</head>
<body>
<h2>Tweets by ${user}</h2>
<div class="box">
<div class="box-content">
<div>What are you doing right now?</div>
<form action="/home" method="post">
<textarea name="newTweet.text" rows="5" cols="60" />
<input type="submit" value="update"/>
</form>
</div>
</div>
@Repeat(items=tweets, var="tweet")
<div class="box">
<div class="box-content">
${tweet.text} (${tweet.createdOn})
</div>
</div>
<a href="http://manning.com/prasanna">Help</a> |
<a href="?action=logout">Sign out</a>
</body>
</html>
模板中的一些关键部分解释如下:
-
<h2>Tweets by ${user}</h2>
:通过调用
HomePage.getUser()
方法动态显示当前用户的用户名。
-
@Repeat(items=tweets, var="tweet")
:告诉 Google Sitebricks 重复渲染
<div class="box">
标签,显示用户发布的所有 tweets。
- 表单部分允许用户输入新的 tweet 并提交,Google Sitebricks 会将表单数据绑定到
HomePage
类的
newTweet
属性上。
6. 总结
通过构建 Crosstalk 应用,我们深入了解了如何在实际项目中应用依赖注入。依赖注入使得代码更加模块化、可维护和可测试,同时提高了代码的可扩展性。在开发过程中,我们使用 Google Guice 管理对象之间的依赖关系,使用 Google Sitebricks 渲染网页,使用 Hibernate 实现数据的持久化。通过合理的包结构和模块化设计,降低了模块之间的耦合度,使得代码更加灵活和易于维护。希望本文能够帮助你更好地理解和应用依赖注入。
下面是 Crosstalk 应用的主要流程 mermaid 流程图:
graph LR
A[用户请求] --> B[GuiceFilter]
B --> C{请求类型}
C -->|静态资源| D[Servlet 容器处理]
C -->|应用请求| E[Google Sitebricks 处理]
E --> F[调用 HomePage 类]
F -->|GET 请求| G[加载用户 tweets]
F -->|POST 请求| H[添加新 tweet 到数据存储]
G --> I[渲染 HomePage 模板]
H --> I
I --> J[返回页面给用户]
通过以上步骤,我们完成了一个简单的 Twitter 克隆应用的构建,并且充分利用了依赖注入的优势。在实际开发中,我们可以根据需求进一步扩展和优化这个应用。
依赖注入实战:构建 Twitter 克隆应用
7. 持久化层
持久化层负责将数据存储到数据库中,并在需要时从数据库中读取数据。在 Crosstalk 应用中,我们使用 Hibernate 作为持久化框架,结合 Hypersonic(HSQL)内存数据库。
7.1 配置 Hibernate
为了使用 Hibernate,我们需要进行一些配置。首先,确保在项目中添加了 Hibernate 相关的依赖库,如
hibernate3.jar
、
hibernate-annotations.jar
等。
然后,创建 Hibernate 的配置文件
hibernate.cfg.xml
,示例如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<!-- 数据库连接配置 -->
<property name="hibernate.connection.driver_class">org.hsqldb.jdbcDriver</property>
<property name="hibernate.connection.url">jdbc:hsqldb:mem:crosstalk</property>
<property name="hibernate.connection.username">sa</property>
<property name="hibernate.connection.password"></property>
<!-- 数据库方言 -->
<property name="hibernate.dialect">org.hibernate.dialect.HSQLDialect</property>
<!-- 自动创建表 -->
<property name="hibernate.hbm2ddl.auto">update</property>
<!-- 显示 SQL 语句 -->
<property name="hibernate.show_sql">true</property>
<!-- 映射实体类 -->
<mapping class="com.wideplay.crosstalk.tweets.Tweet"/>
</session-factory>
</hibernate-configuration>
这个配置文件指定了数据库连接信息、数据库方言、自动创建表的策略以及需要映射的实体类。
7.2 实体类定义
在
com.wideplay.crosstalk.tweets
包中,定义
Tweet
实体类,示例如下:
package com.wideplay.crosstalk.tweets;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import java.util.Date;
@Entity
public class Tweet {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String author;
private String text;
private Date createdOn;
// 构造函数、getters 和 setters
public Tweet() {
}
public Tweet(String author, String text) {
this.author = author;
this.text = text;
this.createdOn = new Date();
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public Date getCreatedOn() {
return createdOn;
}
public void setCreatedOn(Date createdOn) {
this.createdOn = createdOn;
}
}
使用 JPA 注解将
Tweet
类映射到数据库表中。
7.3 数据访问对象(DAO)
为了方便对
Tweet
实体进行数据库操作,我们创建一个数据访问对象(DAO)类。示例如下:
package com.wideplay.crosstalk.services;
import com.wideplay.crosstalk.tweets.Tweet;
import javax.inject.Singleton;
import java.util.List;
@Singleton
public interface TweetManager {
List<Tweet> tweetsFor(String username);
void addTweet(Tweet tweet);
}
package com.wideplay.crosstalk.services.impl;
import com.wideplay.crosstalk.tweets.Tweet;
import com.wideplay.crosstalk.services.TweetManager;
import javax.inject.Singleton;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.List;
@Singleton
public class TweetManagerImpl implements TweetManager {
@PersistenceContext
private EntityManager entityManager;
@Override
public List<Tweet> tweetsFor(String username) {
return entityManager.createQuery("SELECT t FROM Tweet t WHERE t.author = :username", Tweet.class)
.setParameter("username", username)
.getResultList();
}
@Override
public void addTweet(Tweet tweet) {
entityManager.persist(tweet);
}
}
TweetManager
接口定义了对
Tweet
实体的操作方法,
TweetManagerImpl
类实现了这些方法,使用 JPA 的
EntityManager
进行数据库操作。
8. 安全层
安全层负责用户认证和页面安全,确保只有经过认证的用户才能访问特定页面。
8.1 用户认证
在 Crosstalk 应用中,我们需要实现用户认证功能。可以创建一个
User
类来表示用户信息,并创建一个
UserService
类来处理用户认证逻辑。示例如下:
package com.wideplay.crosstalk.tweets;
public class User {
private String username;
private boolean authenticated;
public User(String username) {
this.username = username;
this.authenticated = false;
}
public String getUsername() {
return username;
}
public boolean isAuthenticated() {
return authenticated;
}
public void authenticate() {
this.authenticated = true;
}
public void logout() {
this.authenticated = false;
}
}
package com.wideplay.crosstalk.services;
import com.wideplay.crosstalk.tweets.User;
import javax.inject.Singleton;
@Singleton
public interface UserService {
User authenticate(String username, String password);
}
package com.wideplay.crosstalk.services.impl;
import com.wideplay.crosstalk.tweets.User;
import com.wideplay.crosstalk.services.UserService;
import javax.inject.Singleton;
@Singleton
public class UserServiceImpl implements UserService {
@Override
public User authenticate(String username, String password) {
// 简单示例,实际中应验证用户名和密码
User user = new User(username);
user.authenticate();
return user;
}
}
User
类表示用户信息,
UserService
接口定义了用户认证方法,
UserServiceImpl
类实现了该方法。
8.2 页面安全
为了确保页面安全,我们可以创建一个过滤器来验证用户是否已经认证。示例如下:
package com.wideplay.crosstalk.web;
import com.wideplay.crosstalk.tweets.User;
import javax.inject.Inject;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebFilter(urlPatterns = {"/home"})
public class AuthenticationFilter implements Filter {
@Inject
private User user;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
if (!user.isAuthenticated()) {
httpResponse.sendRedirect("/login?message=Please login.");
return;
}
chain.doFilter(request, response);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void destroy() {
}
}
这个过滤器会拦截
/home
路径的请求,如果用户未认证,则重定向到登录页面。
9. 测试与部署
在完成开发后,我们需要对 Crosstalk 应用进行测试和部署。
9.1 单元测试
可以使用 JUnit 和 Mockito 等工具对各个模块进行单元测试。例如,对
TweetManager
类进行单元测试:
import com.wideplay.crosstalk.tweets.Tweet;
import com.wideplay.crosstalk.services.TweetManager;
import com.wideplay.crosstalk.services.impl.TweetManagerImpl;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import java.util.Arrays;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class TweetManagerTest {
@Test
public void testTweetsFor() {
EntityManager entityManager = Mockito.mock(EntityManager.class);
TypedQuery<Tweet> query = Mockito.mock(TypedQuery.class);
List<Tweet> tweets = Arrays.asList(new Tweet("user1", "Tweet 1"));
Mockito.when(entityManager.createQuery(Mockito.anyString(), Mockito.eq(Tweet.class))).thenReturn(query);
Mockito.when(query.setParameter(Mockito.anyString(), Mockito.anyString())).thenReturn(query);
Mockito.when(query.getResultList()).thenReturn(tweets);
TweetManager tweetManager = new TweetManagerImpl();
((TweetManagerImpl) tweetManager).setEntityManager(entityManager);
List<Tweet> result = tweetManager.tweetsFor("user1");
assertEquals(tweets, result);
}
}
9.2 部署
将 Crosstalk 应用部署到 Servlet 容器中,如 Apache Tomcat 或 Mort Bay Jetty。确保在部署前已经完成了所有依赖库的添加和配置。
10. 扩展与优化
Crosstalk 应用可以根据需求进行进一步的扩展和优化。
10.1 功能扩展
- 添加评论功能 :允许用户对 tweets 进行评论,需要创建评论实体类和相应的服务类。
- 关注功能 :实现用户之间的关注关系,用户可以关注其他用户并查看他们的 tweets。
10.2 性能优化
- 缓存机制 :使用缓存技术,如 Redis,缓存热门 tweets 或用户信息,减少数据库访问次数。
- 数据库优化 :对数据库进行优化,如添加索引、优化查询语句等。
11. 总结
通过构建 Crosstalk 应用,我们详细介绍了如何在实际项目中应用依赖注入,包括表示层、持久化层和安全层的实现。依赖注入使得代码更加模块化、可维护和可测试,同时提高了代码的可扩展性。在开发过程中,我们使用 Google Guice 管理对象之间的依赖关系,使用 Google Sitebricks 渲染网页,使用 Hibernate 实现数据的持久化,使用自定义的过滤器实现用户认证和页面安全。通过合理的包结构和模块化设计,降低了模块之间的耦合度,使得代码更加灵活和易于维护。
以下是 Crosstalk 应用的整体架构 mermaid 流程图:
graph LR
A[用户] --> B[Web 浏览器]
B --> C[Servlet 容器]
C --> D[GuiceFilter]
D --> E{请求类型}
E -->|静态资源| F[Servlet 容器处理]
E -->|应用请求| G[Google Sitebricks]
G --> H[HomePage 类]
H -->|GET 请求| I[TweetManager 加载 tweets]
H -->|POST 请求| J[TweetManager 添加 tweet]
I --> K[HomePage 模板渲染]
J --> K
K --> L[返回页面给用户]
M[数据库] <--> N[Hibernate]
N <--> I
N <--> J
O[用户认证] <--> P[UserService]
P <--> H
希望本文能够帮助你更好地理解和应用依赖注入,在实际项目中构建出高质量的应用。
超级会员免费看
1287

被折叠的 条评论
为什么被折叠?



