描述
上一篇(一个六年经验的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 void bindService(Class 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 void addMapper(Class mapperType) {
bind(mapperType).toProvider(guicify(new MysqlMapperProvider<>(mapperType))).in(Scopes.SINGLETON);
}
}
import javax.inject.Provider;
public class MysqlMapperProvider implements Provider {
private final Class mapperType;
@Inject
@Named("mysql")
private SqlSessionManager sqlSessionManager;
public MysqlMapperProvider(Class 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 getList(Boolean isActive, Integer offset, Integer limit) {
List 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 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 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