简单,易于Mock,仅依赖Spring的Domain Model

本文介绍了一种在Java中实现富领域模型的方法,解决了领域对象与数据库逻辑的依赖问题,使得领域模型易于单元测试,同时保持了代码的简洁性和易用性。

声明:该贴并不讨论Domain Model对于企业应用是否有意义,JE已经讨论过很多了,有很多优秀的帖供参考,这里仅谈实现。

马丁大叔在《企业应用架构模式中》提出Domain Model(领域模型,领域对象,Domain Object)的概念后,我们发现这才是开发企业应用更OO的模式,以前的Transaction Script简直太土了。。。按照领域模型组织起来的面向对象语言代码,简直美极了,而ROR中的ActiveRecord就是一种Domain Model的实现,其威力大家也都见识过了。但是因为Java语言的特性的原因(例如静态类型),一直没有一种好的,优美的,简单的方式去实现,主要的难题就是领域对象和数据库逻辑的依赖关系(例如和Dao的依赖),大家关于这方面的尝试从来就没有断过,目前有以下几种主流的实现方式:

1 放弃IOC,在DomainObject里直接去获得Dao的Instance,例如在Domain Object里拿到Spring上下文,直接get所需要的Bean,这样的优势就是简单,缺点就是DomainObject与Dao的实现以及上下文的耦合,领域逻辑的执行依赖数据库,无法单元测试。有的观点认为利用内存数据库,这样比真正的访问数据库要快,要方便,认为mock dao没有意义,这个观点还持保留意见 :)

2 利用一些框架的特性去实现Dao或者数据库访问能力的注入,例如利用Hibernate Interceptor,在Domain Object持久化的时候注入,这个方式有个明显的缺点就是Domain Object的生命周期依赖特定环境,也就是说这个Domain Object必须是由Hibernate构造的,其才会被注入Dao。这点其实很郁闷,我们以前有个项目就是用这种方式,自己写了Hibernate Interceptor去注入Domain Object,初看很爽很EASY,但是表现层这边,DWR将用户提交的表单数据转换成DomainObject后,那个Dao当然就是NULL了,于是自己又写了个DWR的 Convertor,去实现注入,而这个Domain Object真的是很Rich,随后又要通过web serivce传输,又要在webservice框架里加拦截器实现注入,真的是很郁闷。

3 将Dao通过方法参数传入,优点就是简单,POJOS In Action里就提到了这种方式,缺点就是会影响方法以及接口的设计,经常会出现一个方法会有6个参数之多,而5个都是Dao,真是很丑陋。

4 利用AspectJ等工具实现编译期注入,Spring 2.0刚出的时候,引入了AspectJ,其Reference中就提到了这个方式,我也尝试过,非常的麻烦,而且性能很慢,没记错的话当时大概是06年10月份,用的JDK5,现在不知道有没有改善,我当时还在JE发过新手帖询问过,某大牛说是BEA的虚拟机对其有优化,我也没条件尝试,而且编译期的手脚动太多了,感觉不是很好。

上述几种方式都有明显的缺点,我们追求的好的领域模型的实现就是,领域模型不依赖实现,例如可以注入,易于单元测试,而且其生命周期不依赖特定的环境,还要够简单!难道真是Robbin所说的JAVA不适合Rich Domain Model吗?不见得:

首先,不依赖实现,可以注入,那么要在领域模型中提供接口dao的set方法,允许被注入,这个很明确。

其次,领域模型的生命周期不依赖环境, 不依赖hibernate的构造,不被spring管理等。一般的情况,dao都是无状态的,单例的,那么把领域模型中的依赖的dao设置为static,static是class级别的,和具体的instance没关系,只要在应用初始化的时候给所有的领域模型的class注入一次,整个应用运行的时间周期,领域模型随意怎么new,在哪new,new出来的Domain Object的都可以访问一开始被注入的static的dao,实现了领域模型的生命周期不依赖环境,这样也易于测试,易于MOCK,和目前传统编程方式差别很小,仅仅把dao改为static的。

 

但是spring可以实现对一个class的静态peroperty注入吗?答案是不能(经bottom同学修正,spring是可以静态property注入,但是还是要在所有bean初始化之前注入才行,所以还是要扩展,我在3楼已经回复说明),但是spring是个优美扩展性强的框架,我们只要稍微扩展一下,就可以达到我们的目的。

就拿一个典型的web应用为例,普遍的方式的是在web.xml配置spring的context loader listener和spring的配置文件,这个listener会在应用启动时,创建并初始化spring的上下文,我们需要在所有bean初始化之前注入domain class,那么就在这里找切入点,通过spring的源代码可以发现类AbstractApplicationContext的refresh方法内的finishBeanFactoryInitialization有些重要的东东。

Java代码 复制代码
  1. /**  
  2.  * Finish the initialization of this context's bean factory,  
  3.  * initializing all remaining singleton beans.  
  4. */  
  5. protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {   
  6.        // 省略   
  7.        // Instantiate all remaining (non-lazy-init) singletons.   
  8.         // 这里,会初始化所有的单例   
  9.        beanFactory.preInstantiateSingletons();   
  10. }  
/**
 * Finish the initialization of this context's bean factory,
 * initializing all remaining singleton beans.
*/
protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
       // 省略
       // Instantiate all remaining (non-lazy-init) singletons.
        // 这里,会初始化所有的单例
       beanFactory.preInstantiateSingletons();
}


通过观察代码发现,初始化单例其实就是将Spring配置文件中定义的所有不是lazy init的单例初始化了,对一个Bean的初始化就是调用getBean来完成,Spring在调用getBean的时候,会判断如果已经初始化了就直接返回给你Bean的引用,否则就初始化这个Bean再返回给你,很典型的实现LAZY的方式。

我们只需要在这个初始化方法之前,主动去扫描所有的领域模型class,并且知道他们需要注入的Dao,直接getBean获得dao的实例,并且注入进Domain Model的class就完成了,那么我们只需要自己实现一个ConfigurableListableBeanFactory接口,也就是继承DefaultListableBeanFactory(其是ConfigurableListableBeanFactory接口的默认实现), 并且在preInstantiateSingletons方法的时候先注入领域模型,巨简单

Java代码 复制代码
  1. private class StaticPropertyInjectSupportListableBeanFactory extends DefaultListableBeanFactory {   
  2.        @Override//覆盖父类的方法   
  3.        public void preInstantiateSingletons() throws BeansException {   
  4.               //先注入所有的领域模型的class   
  5.               injectStaticPropertyForBasePackage();   
  6.               //再调用父类的该方法去初始化所有的非LAZY单例   
  7.               super.preInstantiateSingletons();   
  8.        }   
  9. }  
private class StaticPropertyInjectSupportListableBeanFactory extends DefaultListableBeanFactory {
       @Override//覆盖父类的方法
       public void preInstantiateSingletons() throws BeansException {
              //先注入所有的领域模型的class
              injectStaticPropertyForBasePackage();
              //再调用父类的该方法去初始化所有的非LAZY单例
              super.preInstantiateSingletons();
       }
}

继续观察代码,finishBeanFactoryInitialization的方法的参数ConfigurableListableBeanFactory,是通过调用的createBeanFactory方法获得的,默认的实现在AbstractRefreshableApplicationContext中有这样一个方法,去创建DefaultListableBeanFactory,而这个方法是protected的,摆明着让我们去复盖扩展其功能,这也是spring易于扩展的体现,我们只要实现一个自己的ApplicaitonContext,覆盖createBeanFactory方法创建我们刚才自己实现的StaticPropertyInjectSupportListableBeanFactory就OK了,代码也巨简单

Java代码 复制代码
  1. public class StaticPropertyInjectSupportXmlWebApplicationContext extends XmlWebApplicationContext {   
  2.        @Override  
  3.        protected DefaultListableBeanFactory createBeanFactory() {   
  4.               return new StaticPropertyInjectSupportListableBeanFactory(getInternalParentBeanFactory());   
  5.        }   
  6. }  
public class StaticPropertyInjectSupportXmlWebApplicationContext extends XmlWebApplicationContext {
       @Override
       protected DefaultListableBeanFactory createBeanFactory() {
              return new StaticPropertyInjectSupportListableBeanFactory(getInternalParentBeanFactory());
       }
}


实现了自己的ApplicationContext,怎么让Spring启动的时候初始化你的上下文呢?当然可以了,spring提供了扩展的后门,只要在web.xml配置context parameter "contextClass"告诉spring,他就会用你自己的ApplicationContext去初始化了,不配置该参数默认的ApplicationContext实现是XmlWebApplicationContext。

关于自己实现静态property的注入就很简单了,在context parameter里加入个参数configurableBeanBasePackage,获得所有要注入的领域模型的父包,就可以了,如下

Xml代码 复制代码
  1. <!--加入这两个参数,其余不变 -->  
  2. <!--告诉spring用我们自己实现的支持静态bean注入的context -->  
  3. <context-param>  
  4.         <param-name>contextClass</param-name>  
  5.     <param-value>com.norther.sps.StaticPropertyInjectSupportXmlWebApplicationContext</param-value>  
  6. </context-param>  
  7. <!--注入com.norther包下(包含子包)所有的领域模型的class-->  
  8. <context-param>     
  9.     <param-name>configurableBeanBasePackage</param-name>  
  10.     <param-value>com.norther</param-value>  
  11. </context-param>  


这样就OK了,有人会想到这样还是有一点点点点麻烦,用PostProcessorBean或者Spring的容器初始化完毕的事件去实现注入static property不是更爽吗?这也是我最先想到的方式,但是这两种方式的注入的时间太靠后了,那个preInstantiateSingletons方法已经被调用过了,有些bean已经被初始化了,例如quartz的SchedulerFactoryBean,就有可能你部署的quartz或者其他的 job已经开始跑了,但是领域模型还没有被注入,在那些job里调用领域模型都会空指针,所以一定要在preInstantiateSingletons方法调用前去注入。

 

那么如何辨别哪些是领域模型呢?就利用了spring的Configurable这个annotation去标识这个是需要静态注入的领域模型,然后自己实现了两种自动装配,BY_TYPE和BY_NAME,以及JSR-250的Resource annotation,例子如下

Java代码 复制代码
  1. @Configurable  
  2. public class Student {   
  3.        private Long id;   
  4.   
  5.        @Resource //注入field的名字所代表的bean,   
  6.        private static StudentDao studentDao;   
  7.   
  8.        public static void setStudentDao(StudentDao studentDao) {   
  9.               Student.studentDao = studentDao;   
  10.        }  
@Configurable
public class Student {
       private Long id;

       @Resource //注入field的名字所代表的bean,
       private static StudentDao studentDao;

       public static void setStudentDao(StudentDao studentDao) {
              Student.studentDao = studentDao;
       }

 

Java代码 复制代码
  1. @Configurable  
  2. public class Assistant {   
  3.        private static StudentDao studentDao;   
  4.   
  5.        @Resource(name = "studentDao"//注入name为studentDao的bean    
  6.        public static void setDao(StudentDao studentDao) {   
  7.               Assistant.studentDao = studentDao;   
  8.        }  
@Configurable
public class Assistant {
       private static StudentDao studentDao;

       @Resource(name = "studentDao") //注入name为studentDao的bean 
       public static void setDao(StudentDao studentDao) {
              Assistant.studentDao = studentDao;
       }
Java代码 复制代码
  1. @Configurable(autowire = Autowire.BY_TYPE)//自动装配   
  2. public class SchoolMaster {   
  3.        private static StudentDao studentDao;   
  4.   
  5.        // by type   
  6.        public static void setDao(StudentDao studentDao) {   
  7.               SchoolMaster.studentDao = studentDao;   
  8.        }  
@Configurable(autowire = Autowire.BY_TYPE)//自动装配
public class SchoolMaster {
       private static StudentDao studentDao;

       // by type
       public static void setDao(StudentDao studentDao) {
              SchoolMaster.studentDao = studentDao;
       }

单元测试以及Mock

Java代码 复制代码
  1. @Configurable(autowire = Autowire.BY_NAME)//自动装配   
  2. public class Teacher {   
  3.        private static StudentDao studentDao;   
  4.   
  5.        public static void setStudentDao(StudentDao studentDao) {   
  6.               Teacher.studentDao = studentDao;   
  7.        }   
  8.   
  9.        public List<Students> getStudents() {   
  10.               return studentDao.getByClassId(this.getClassId());   
  11.        }  
@Configurable(autowire = Autowire.BY_NAME)//自动装配
public class Teacher {
       private static StudentDao studentDao;

       public static void setStudentDao(StudentDao studentDao) {
              Teacher.studentDao = studentDao;
       }

       public List<Students> getStudents() {
              return studentDao.getByClassId(this.getClassId());
       }

 

 

Java代码 复制代码
  1. StudentDao studentDao = EasyMock.createMock(StudentDao.class);   
  2. EasyMock.expected(studentDao.getByClassId(998877)).andReturn(students);   
  3.   
  4. Teacher.setStudentDao(studentDao);   
  5. Teacher zhang3 = new Teacher();   
  6. zhang3.setClassId(998877);//设置班级ID   
  7. List<Student> actual = zhang3.getStudents();   
  8. .......  
 StudentDao studentDao = EasyMock.createMock(StudentDao.class);
 EasyMock.expected(studentDao.getByClassId(998877)).andReturn(students);

 Teacher.setStudentDao(studentDao);
 Teacher zhang3 = new Teacher();
 zhang3.setClassId(998877);//设置班级ID
 List<Student> actual = zhang3.getStudents();
 .......


后半部分说了这么多其实都是实现static property的注入,原理很简单,代码也很简单,就4个类,但是我认为这么简单的东西spring之类的框架可以更容易并且实现的更好,也就是说目前的java可以简单的解决领域对象和dao依赖的问题从而实现rich domain model。

 

当然这个代码只是demo性质的,实现的比较简单,只有web方式的实现方法,希望和大家共同讨论。

<think> 我们正在讨论的是Spring Cloud项目的代码分层设置方法。参考之前的引用,特别是引用[4]中提到的Spring Boot项目结构,以及引用[2][3]中关于Spring Cloud配置和服务发现的组件,我们可以构建一个典型的Spring Cloud微服务项目分层结构。 注意:在Spring Cloud微服务架构中,每个微服务都是一个独立的Spring Boot应用,因此每个微服务内部的分层遵循Spring Boot的约定,同时结合微服务架构的特点(如服务注册发现、配置中心等)进行设计。 以下是一个标准的Spring Cloud微服务项目(单个微服务)的分层设置方法: 1. **项目结构(基于Maven)**: 根据引用[4]的约定,项目根目录结构如下: ``` project-root/ ├── src/ │ ├── main/ │ │ ├── java/ # Java源代码 │ │ ├── resources/ # 配置文件和静态资源 │ ├── test/ # 测试代码 ├── pom.xml # 项目依赖管理 ``` 2. **Java源代码分层(在src/main/java下)**: 通常我们按照功能模块(如用户模块、订单模块)或技术层次(如controller、service等)来组织包结构。常见的分层包结构如下: ``` com.example.microservice ├── config # 配置类(如Spring Cloud Config Client配置、Feign配置等) ├── controller # 控制器层(REST API入口) ├── service # 服务层(业务逻辑) │ ├── impl # 服务实现类 ├── dao/repository # 数据访问层(JPA、MyBatis等) ├── entity/model/domain # 实体类(与数据库表对应的POJO) ├── dto # 数据传输对象(用于接口传输) ├── feign # Feign客户端接口(用于服务间调用) ├── exception # 自定义异常 ├── util # 工具类 ├── Application.java # 主启动类 ``` 3. **各层职责说明**: - **Controller层**:处理HTTP请求,调用Service层,返回响应。使用`@RestController`注解。 - **Service层**:实现业务逻辑,可以调用DAO层或其他服务(通过Feign)。使用`@Service`注解。 - **DAO/Repository层**:负责数据库操作,使用Spring Data JPA或MyBatis等。使用`@Repository`注解。 - **Entity层**:定义与数据库表映射的Java实体类,使用JPA注解(如`@Entity`)或MyBatis注解。 - **DTO层**:用于Controller与前端交互的数据封装,避免直接暴露实体类。 - **Feign客户端**:声明式服务调用,定义其他微服务的接口。 - **Config层**:存放Spring配置类,如`@Configuration`类,用于配置Bean、Feign、拦截器等。 4. **Spring Cloud组件的集成**: - **服务注册与发现(Eureka)**:在主启动类上使用`@EnableEurekaClient`(引用[3])。同时,在配置文件中配置Eureka服务器地址。 - **配置中心(Spring Cloud Config)**:引用[2]提到,我们可以使用Config Server集中管理配置。在微服务中作为Config Client,需要: - 添加依赖:`spring-cloud-starter-config` - 在`bootstrap.yml`(或`bootstrap.properties`)中指定Config Server的地址和配置文件名。 - **服务调用(Feign或RestTemplate)**:使用Feign时,创建接口并使用`@FeignClient`注解。需要在配置类上使用`@EnableFeignClients`。 5. **配置文件**: 在`src/main/resources`目录下,通常有: - `application.yml`(或`application.properties`):通用配置(如服务端口、应用名等)。 - `bootstrap.yml`:优先加载,用于配置中心连接信息(因为配置中心的信息需要在应用启动早期获取)。 6. **代码示例**: **主启动类**: ```java package com.example.microservice; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; import org.springframework.cloud.openfeign.EnableFeignClients; @SpringBootApplication @EnableEurekaClient @EnableFeignClients(basePackages = "com.example.microservice.feign") public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } ``` **Controller示例**: ```java package com.example.microservice.controller; import com.example.microservice.dto.UserDTO; import com.example.microservice.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/users") public class UserController { @Autowired private UserService userService; @GetMapping("/{id}") public UserDTO getUser(@PathVariable Long id) { return userService.getUserById(id); } } ``` **Feign客户端示例**: ```java package com.example.microservice.feign; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @FeignClient(name = "order-service") // 服务名 public interface OrderFeignClient { @GetMapping("/orders/{userId}") List<OrderDTO> getOrdersByUserId(@PathVariable Long userId); } ``` 7. **测试**: 在`src/test/java`下建立与主代码结构相同的包结构,编写单元测试和集成测试。使用`@SpringBootTest`进行集成测试。 8. **注意事项**: - 遵循单一职责原则,每个类只负责一个功能。 - 使用接口和实现分离的方式(如Service接口和ServiceImpl)以方便扩展和Mock测试。 - 使用依赖注入(`@Autowired`),但避免循环依赖。 通过以上分层,可以构建一个清晰、可维护的Spring Cloud微服务。 §§相关问题§§ 1. 如何在Spring Cloud微服务中实现配置的动态更新? 2. Spring Cloud微服务之间如何保证数据一致性? 3. Spring Cloud微服务分层架构中,如何实现跨模块的公共代码复用? 4. 在微服务架构中,如何设计异常处理机制? </think> ### Spring Cloud代码分层设置方法 在Spring Cloud微服务架构中,代码分层需要结合三层架构思想和微服务特性。以下是标准分层方案(以Maven项目为例): #### 1. 基础分层结构(核心三层) ```markdown src/main/java ├── com.example.service │ ├── controller # 控制层(API入口) │ ├── service # 业务逻辑层 │ │ ├── impl # 服务实现类 │ ├── dao/repository # 数据访问层 │ ├── entity # 实体类(数据库映射) │ ├── dto # 数据传输对象 │ └── config # 配置类 ``` - **控制层(Controller)**:处理HTTP请求/响应 ```java @RestController @RequestMapping("/users") public class UserController { @Autowired private UserService userService; // 依赖业务层 } ``` - **业务逻辑层(Service)**:实现核心业务规则 ```java @Service public class UserServiceImpl implements UserService { @Autowired private UserRepository userRepository; // 依赖数据层 public UserDTO getUserById(Long id) { // 业务逻辑处理 return convertToDTO(userRepository.findById(id)); } } ``` - **数据访问层(Repository)**:数据库操作 ```java @Repository public interface UserRepository extends JpaRepository<User, Long> { // Spring Data JPA自动实现 } ``` #### 2. Spring Cloud特有分层 ```markdown src/main/java ├── com.example.service │ ├── feign # Feign客户端(服务间调用) │ ├── fallback # 熔断降级实现 │ ├── filter # 网关过滤器 │ ├── config │ │ ├── cloud # 云配置(Eureka, Config等) ``` - **服务通信层(Feign)**:声明式服务调用 ```java @FeignClient(name = "order-service", fallback = OrderFallback.class) public interface OrderFeignClient { @GetMapping("/orders/{userId}") List<Order> getOrders(@PathVariable Long userId); } ``` - **配置中心集成**:集中管理配置(引用[2]) ```yaml # bootstrap.yml spring: cloud: config: uri: http://config-server:8888 # 配置中心地址[^2] ``` #### 3. 关键配置项 1. **依赖配置**(引用[3]) ```xml <!-- pom.xml --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> ``` 2. **分层配置原则**: - 控制层:处理参数校验/格式转换 - 业务层:包含核心逻辑($ \text{业务结果} = f(\text{输入参数}) $) - 数据层:只做CRUD操作 #### 4. 分层调用流程 ```mermaid graph TD A[API Gateway] --> B(Controller) B --> C(Service) C --> D[Feign调用其他服务] C --> E(Repository) E --> F[(数据库)] D --> G[Service B] ``` #### 5. 最佳实践 1. **严格层级调用**: - Controller → Service → Repository - 禁止跨层调用(如Controller直接访问Repository) 2. **DTO隔离**: ```java // 避免直接暴露实体类 public class UserDTO { private String name; private String email; // 无敏感字段 } ``` 3. **配置分离**(引用[4]): ``` src/main/resources ├── application.yml # 通用配置 ├── bootstrap.yml # 启动配置 └── /config # 环境特定配置 ├── dev.yml └── prod.yml ``` 4. **熔断降级**: ```java @Component public class OrderFallback implements OrderFeignClient { public List<Order> getOrders(Long userId) { // 返回兜底数据 } } ``` #### 6. 分层优势 1. **解耦性**:修改数据库不影响API接口(符合MVC分离思想[^1]) 2. **可维护性**:业务变化只需修改Service层 3. **可测试性**:各层可独立单元测试 4. **云原生支持**:无缝集成Spring Cloud组件(Eureka[^3], Config[^2])
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值