描述

上一篇(一个六年经验的python后端是怎么学习用java写API的(4) RestAPI,dropwizard 的第一组API)写完第一组API后发现,每次实现一个resource,都需要在 Application.java里面的sessionFactory的config里面addMapper,然后在具体使用mybatis查询的时候也需要先拿到 session 然后再把这个mapper类拿出来,非常不方便 session.getMapper(ArticleMapper.class)

查阅资料发现java的一些web框架是通过一种叫做依赖注入的方式减少这些操作的,google-guice的这个wiki就讲解了依赖注入的核心。大概就是说,代码不需要导出new东西了,通过@inject注解减少很多代码。

代码

parrot tag: google_injector

依赖注入实现

https://github.com/google/gui...

大概就是需要继承 AbstractModule 然后 通过 configure 将各种类注册,然后需要的地方可以直接使用 injector.getInstance(注册的类.class);,或者直接在构造方法等地方使用 @Inject 等注解。

代码结构如下,可以看到主要是添加了module 和 service

 $ tree
.
└── com
    ├── loginbox
    │   └── dropwizard
    │       └── mybatis
    │           └── EnhancedMybatisBundle.java
    └── reworkplan
        ├── ParrotApplication.java
        ├── bundles
        │   ├── CorsBundle.java
        │   ├── GuiceBundle.java
        │   └── MysqlBundle.java
        ├── common
        │   ├── Constants.java
        │   └── response
        │       ├── MetaListResponse.java
        │       └── MetaMapperResponse.java
        ├── config
        │   └── ParrotConfiguration.java
        ├── mappers
        │   ├── ArticleMapper.java
        │   └── handlers
        │       └── DateTimeTypeHandler.java
        ├── models
        │   └── Article.java
        ├── modules
        │   ├── ApplicationModule.java
        │   ├── InjectorFactory.java
        │   ├── MysqlMapperModule.java
        │   ├── ServiceModule.java
        │   └── provider
        │       └── MysqlMapperProvider.java
        ├── resources
        │   ├── ArticlesResource.java
        │   └── BasePath.java
        └── service
            ├── ArticleService.java
            └── ArticleServiceImpl.java        

InjectorFactory 的 createInjector 里面添加需要注册的各个Module,有些需要拿到config上下文。

public class InjectorFactory {  
    public Injector get(ParrotConfiguration configuration, Environment environment) {  
        Injector intjector = Guice.createInjector(  
                new ApplicationModule(configuration, environment),  
                new MysqlMapperModule(configuration, environment),  
                new ServiceModule()  
        );  
        return intjector ;  
    }  
}

先说简单的 service module,django的api的重逻辑是建议直接写在models.py里面的,跟django的逻辑不同的是,java除了直接操作数据库的dao层外,还需要一个service 层,而且很烦的一点在于要先实现一个service 抽象类,在实现一个serviceImpl的实现类,所以这个model主要就是将抽象类和实现类绑定。

public class ServiceModule extends AbstractModule {

    @Override
    protected void configure() {
        bindService(ArticleService.class, ArticleServiceImpl.class);
    }

    private <T> void bindService(Class<T> interfaceClass, Class<? extends T> implClass) {
        bind(interfaceClass).to(implClass).in(Scopes.SINGLETON);
    }
}

写完这个之后再 Application.java 里面就可以直接这么写,environment.jersey().register(injector.getInstance(ArticlesResource.class));,然后在Resource初始化的时候直接使用 @Inject注解就完事了。

public class ArticlesResource {
    private final ArticleService articleService;

    @Inject
    public ArticlesResource(ArticleService articleService) {
        this.articleService = articleService;
    }
    ...
}

MysqlMapperModule类似,核心逻辑在于将 Mapper类在 config上注册,而之前知道在注册mapper的时候其实还用到了sessionFactory,所以还要在写一个实现了javax.inject.Provider的 MysqlMapperProvider

public class MysqlMapperModule extends AbstractModule {
    private final ParrotConfiguration configure;
    private final Environment enviroment;

    public MysqlMapperModule(ParrotConfiguration configuration, Environment environment) {
        this.configure = configuration;
        this.enviroment = environment;
    }

    @Override
    protected void configure() {
        addMapper(ArticleMapper.class);
    }

    @Provides
    @Singleton
    @Named("mysql")
    public SqlSessionManager getMysqlSqlSessionManager(@Named("mysql") SqlSessionFactory sqlSessionFactory) {
        return SqlSessionManager.newInstance(sqlSessionFactory);
    }

    @Provides
    @Singleton
    @Named("mysql")
    public SqlSessionFactory getMysqlSqlSessionFactory() {
        return this.configure.getMysqlSqlSessionFactory();
    }

    private <T> void addMapper(Class<T> mapperType) {
        bind(mapperType).toProvider(guicify(new MysqlMapperProvider<>(mapperType))).in(Scopes.SINGLETON);
    }
}

import javax.inject.Provider;

public class MysqlMapperProvider <T> implements Provider<T> {

    private final Class<T> mapperType;

    @Inject
    @Named("mysql")
    private SqlSessionManager sqlSessionManager;

    public MysqlMapperProvider(Class<T> mapperType) {
        this.mapperType = mapperType;
    }

    public void setSqlSessionManager(SqlSessionManager sqlSessionManager) {
        this.sqlSessionManager = sqlSessionManager;
    }

    @Override
    public T get() {
        return this.sqlSessionManager.getMapper(mapperType);

    }
}

写完这个之后对应的 serviceImpl 和 resouce这么写,

public class ArticleServiceImpl implements ArticleService {
    private final ArticleMapper articleMapper;

    @Inject
    public ArticleServiceImpl(ArticleMapper articleMapper) {
        this.articleMapper = articleMapper;
    }

    @Override
    public List<Article> getList(Boolean isActive, Integer offset, Integer limit) {
        List<Article> articles = articleMapper.selectAll(isActive, offset, limit);
        return articles;
    }

    @Override
    public Article get(Integer id, Boolean isActive) {
        return articleMapper.select(id, isActive);
    }

    @Override
    public Integer count(Boolean isActive) {
        return articleMapper.countAll(isActive);
    }
}

@Path(BasePath.ARTICLE_API)
@Produces(APPLICATION_JSON)
public class ArticlesResource {
    private final ArticleService articleService;

    @Inject
    public ArticlesResource(ArticleService articleService) {
        this.articleService = articleService;
    }

    @GET
    @Timed
    public MetaListResponse get(@DefaultValue(Constants.DEFAULT_PARAM_OFFSET) @QueryParam("offset") Integer offset,
                                @DefaultValue(Constants.DEFAULT_PARAM_LIMIT) @QueryParam("limit") Integer limit){
        MetaListResponse response = new MetaListResponse();
        response.putMeta("count", 0);
        Boolean isActive = true;
        Integer count = articleService.count(isActive);
        List<Article> articles = articleService.getList(isActive, offset, limit);
        response.putMeta("count", count);
        response.setData(articles);
        return response;
    }

    @Path("/{id}")
    @GET
    @Timed
    public MetaMapperResponse get(@NotNull @PathParam("id") Integer articleId) {
        MetaMapperResponse response = new MetaMapperResponse();
        Boolean isActive = true;
        Article article = articleService.get(articleId, isActive);
        response.setData(article);
        return response;
    }
}

对比下之前版本的Resource可以发现明显代码干净多了, 不用每次都在sessionFactory去拿Mapper.class了,之后实现完mapper、service后只要在对应的module的config里面添加一行即可。

@Path(BasePath.ARTICLE_API)
@Produces(APPLICATION_JSON)
public class ArticlesResource {
    private final SqlSessionFactory sessionFactory;

    public ArticlesResource(SqlSessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }   

    @GET
    @Timed
    public MetaListResponse get(@DefaultValue(Constants.DEFAULT_PARAM_OFFSET) @QueryParam("offset") Integer offset,
                                @DefaultValue(Constants.DEFAULT_PARAM_LIMIT) @QueryParam("limit") Integer limit){
        MetaListResponse response = new MetaListResponse();
        response.putMeta("count", 0); 
        try (SqlSession session = sessionFactory.openSession()) {
            ArticleMapper articleMapper = session.getMapper(ArticleMapper.class);
            Boolean isActive = true;
            Integer count = articleMapper.countAll(isActive);
            List<Article> articles = articleMapper.selectAll(isActive, offset, limit);
            response.putMeta("count", count);
            response.setData(articles);
        }
        return response;
    }

    @Path("/{id}")
    @GET
    @Timed
    public MetaMapperResponse get(@NotNull @PathParam("id") Integer articleId) {
        MetaMapperResponse response = new MetaMapperResponse();
        try (SqlSession session = sessionFactory.openSession()) {
            ArticleMapper articleMapper = session.getMapper(ArticleMapper.class);
            Boolean isActive = true;
            Article article = articleMapper.select(articleId, isActive);
            response.setData(article);
        }
        return response;
    }
}            

一些感悟

至于为什么java要多出一层service和serviceImpl,看到目前我的感受就是静态语言太过于笨重,语言上有private、public本身的设计模式,model上要实现一个Bean就要写一大堆的东西,而这堆东西仅仅是基本的字段赋值取值,即使用了mybatis做orm,这个model层已然很重了,不适合做多个model之间的数据柔和等逻辑。

而django的model在orm拿出即用,可以说所有属性都是public的,所以不需要封装bean,全public的特性就使得直接揉其他model在语言特性上就轻的多。

当然java可以说我定义了一个service就不用关心具体子类是怎么实现的,但是写工程代码多了可能会听说过这句话,约定大于配置,同样的轻语言在各个不同子方法(类或者直接就是方法)实现的时候只要遵循约定的接口即可。

另外虽然java有强编译的特性,但是很多时候依然会遇到运行时各种空指针问题,所以说大工程下java天生强于轻语言是很蠢的一种说法,工程项目靠的还是大量的测试(单元、集成),部署监控策略(测试环境、灰度、放量上线)。

接下来

todo: 一个六年经验的python后端是怎么学习用java写API的(6) 基本的Auth


D咄咄
1.7k 声望257 粉丝

Life is to short, please use python.