# SpringBean注入规范

V5平台基于Spring框架进行Java后台对象管理,三层模型中的Manager和Dao均要求注册到Spring容器中,采用依赖注入的形式实例化调用。

Spring Bean的注册和引用有很多种方法,但V5有对应规范,我们希望用规范的写法来完成开发,避免出现未知问题。

# 引用已有Bean实例

# 最推荐使用代码

如果你正在开发新功能,此时需要获取组织机构的一些数据,则必然会用到组织机构的核心接口OrgManager。

一个规范的引用Bean方法为:

// 特别注意:DemoManagerImpl一定也要是一个Spring Bean对象,此实例需要交给Spring容器管理
public class DemoManagerImpl implements DemoManager {

	// 第一步:引用OrgManager接口
	private OrgManager orgManager;
	// 第二步:编写set方法,Spring会将实例对象自动注入,详见Spring官方文档
	public void setOrgManager(OrgManager orgManager) {
		this.orgManager = orgManager;
	}
		
	@Override
	public void saveDemo(Map<String, Object> params) throws BusinessException {
		// 第三步:使用orgManager已经实例化的对象
		V3xOrgMember member = orgManager.getMemberById((Long) params.get("memberId"));
		......
	}
}

# 谨慎推荐使用代码

在查阅V5现有代码的时候,大家可能会看到@Inject注解,这个注解引用之后,似乎没有编写set方法也能注入对象。

Inject注解是Ctp依赖注入注解。有别于JSR 330,目前只支持Field注入。

使用@Inject注解之后,就可以省略set方法了,如下代码一般也是有效的:

public class DemoManagerImpl implements DemoManager {

	// 第一步:引用OrgManager接口并引入@Inject注解
	@Inject
	private OrgManager orgManager;
		
	@Override
	public void saveDemo(Map<String, Object> params) throws BusinessException {
		// 第二步:使用orgManager已经实例化的对象
		V3xOrgMember member = orgManager.getMemberById((Long) params.get("memberId"));
		......
	}
}

注意:为什么要谨慎推荐使用? 因为这个注解默认会强制实例化对象,如果Bean对象不存在会抛出异常,致使系统无法启动。

什么时候Bean对象会不存在呢?我们产品是插件化开发,如果客户没买插件,则对应插件下的Bean都不会实例化。比如你引入了任务Bean,但是客户没购买任务插件,使用@Inject注解后会导致无法启动:

public class DemoManagerImpl implements DemoManager {
	
	// 警告:如果task任务插件不存在会导致Bean注入失败并且抛出异常无法启动
	@Inject
	private TaskmanageApi taskmanageApi;
		
	@Override
	public void saveDemo(Map<String, Object> params) throws BusinessException {
		if(taskmanageApi != null){
			TaskInfoBO taskInfoBO = taskmanageApi.getTaskInfo((Long) params.get("taskId"));
		}
		.....
	}
}

针对以上问题,标准产品提供了一个解决方案:追加@PluginQualifier注解,标识这个Bean来自哪个插件,如果带这个注解后,就不会抛出异常了。

如下代码才是最规范的编写方式:

public class DemoManagerImpl implements DemoManager {
	
	// PluginQualifier注解用于标识此Bean属于哪个插件
	@Inject
	@PluginQualifier(pluginName = "taskmanage")
	private TaskmanageApi taskmanageApi;
		
	@Override
	public void saveDemo(Map<String, Object> params) throws BusinessException {
		if(taskmanageApi != null){
			TaskInfoBO taskInfoBO = taskmanageApi.getTaskInfo((Long) params.get("taskId"));
		}
		.....
	}
}

# 没有办法时才使用的方法

需要注意,使用前面两种依赖注入需要要求实现类需要交给Spring容器管理。而我们的Utils工具类一般不会交给Spring,恰恰在Utils中需要使用一些Bena对象时就无法通过依赖注入的形式获取,需要换一种方式获取。

V5平台的AppContext.getBean提供了在方法引用中引用Spring Bean示例的机会:

public class DemoUtils {

    private static Log logger = CtpLogFactory.getLog(DemoUtils.class);

    public static String getMemberName(Long memberId){
        // 通过AppContext.getBean强行获取Bean实例
        OrgManager orgManager = (OrgManager) AppContext.getBean("orgManager");
        try {
            V3xOrgMember member = orgManager.getMemberById(memberId);
            if(member != null){
                return member.getName();
            }
        } catch (BusinessException e) {
            logger.error("",e);
        };
        return "";
    }

}

需要注意AppContext是V5平台封装的API,这是推荐使用的。而Spring本身的ApplicationContext对象虽然也可以获取Bean,但这是不允许使用的,尽量使用V5平台封装,方便后续遇到问题统一调整。

# 绝对禁止编写的代码示例

上一节说到AppContext.getBean可以应急使用,但需要注意此代码绝对不要出现在成员变量后面,如下所示代码是绝对错误,绝对错误,绝对错误!这样会导致Spring初始化的混乱,甚至引发启动异常!

public class DemoManagerImpl implements DemoManager {

	// Bad Code:绝对禁止的代码写法
	private OrgManager orgManager = (OrgManager) AppContext.getBean("orgManager");
		
	@Override
	public void saveDemo(Map<String, Object> params) throws BusinessException {
		V3xOrgMember member = orgManager.getMemberById((Long) params.get("memberId"));
		......
	}
}

有人说“我看到过有些地方这样写是没问题的”,是的,目前唯一允许在成员变量中引入AppContext.getBean的地方是我们的后台Rest接口类

/**Rest接口中直接引用是被允许的,因为他未交给Spring管理,Spring初始化完成之后才初始化Rest接口类,所以不受影响**/
@Path("meeting")
@Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
@Produces(MediaType.APPLICATION_JSON)
@Api(module = ApiModuleEnum.meeting, type = ApiTypeEnum.REST, value = "会议",subModelName = "会议")
public class MeetingResource extends BaseResource {
    private static final Log   LOGGER             = LogFactory.getLog(MeetingResource.class);
    private MtMeetingManager   mtMeetingManager   = (MtMeetingManager) AppContext.getBean("mtMeetingManager");
    private MeetingManager     meetingManager   = (MeetingManager) AppContext.getBean("meetingManager");
    private MeetingM3Manager     meetingM3Manager   = (MeetingM3Manager) AppContext.getBean("meetingM3Manager");
    private MeetingRoomM3Manager meetingRoomM3Manager   = (MeetingRoomM3Manager) AppContext.getBean("meetingRoomM3Manager");
    .....
}

# 如何编写注册一个新的Bean

如果你需要编写一个新的实现类,并将其注册到Spring中,方便自己使用或被别人调用,则按照如下步骤完成即可。

大前提依然是以插件化开发规范进行开发。

首先,定义一个Java接口:

package com.seeyon.apps.demo.manager;
public interface DemoManager {
	public void saveDemo(Map<String,Object> params) throws BusinessException;
}

然后,编写接口的实现类:

package com.seeyon.apps.demo.manager;

public class DemoManagerImpl implements DemoManager {
	@Inject
	private DemoDao demoDao;
		
	@Override
	public void saveDemo(Map<String, Object> params) throws BusinessException {
		Demo demo = new Demo();
		ParamUtil.mapToBean(params, demo, true);
		demo.setIdIfNew();
		demo.setCreateMember(AppContext.currentUserId());
		demo.setCreateTime(new Date());
		demo.setDeleteFlag(Constants4Demo.deleteFlag.ENABLE.ordinal());
		demoDao.save(demo);
	}
}

最后将对象通过XML配置的形式注册到Spring容器中,XML存放于各自插件下:

<!-- 插件化开发,XML注册位置 \seeyon\WEB-INF\cfgHome\plugin\demo\spring\srping-demo-manager.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans default-autowire="byName">
	<bean id="demoManager" class="com.seeyon.apps.demo.manager.DemoManagerImpl"/>
</beans>

最后调用方注册对应Bena即可使用:

public class DemoController extends BaseController{

	private DemoManager demoManager;
    
    public void setDemoManager(DemoManager demoManager) {
		this.demoManager = demoManager;
	}
}

# 红线、禁忌

1)被Spring管理的对象中,依赖注入对象绝对禁止使用AppContext.getBean来完成!

public class DemoManagerImpl implements DemoManager {

	// Bad Code:绝对禁止的代码写法
	private OrgManager orgManager = (OrgManager) AppContext.getBean("orgManager");
	
	@Override
	public void saveDemo(Map<String, Object> params) throws BusinessException {
		V3xOrgMember member = orgManager.getMemberById((Long) params.get("memberId"));
		......
	}
}

2)不要在Spring容器初始化前、初始化中就进行Bean的使用

public class DemoManagerImpl implements DemoManager {
	@Inject
	private DemoDao demoDao;

	DemoManagerImpl(){
		// 不要在构造方法中使用Bean对象
		demoDao.findDemoList();
	}
	
	static {
		// 不要在static代码块中使用Bean对象
		demoDao.findDemoList();
	}
    
    private void init() {
		// 不要在init-method中使用Bean对象
		demoDao.findDemoList();
	}
}

Spring XML中的init-method属性是不推荐使用的,很多人期望在初始化对象时同时进行缓存初始化,这种写法是极其危险的,我们有标准的初始化接口,在后面介绍。

<beans default-autowire="byName">
	<!-- init-method是不允许被使用的 -->
	<bean init-method="init" id="demoManager" class="com.seeyon.apps.demo.manager.DemoManagerImpl" />
</beans>

Spring XML中的init-method的统一替代方案如下:

平台提供了一个标准组件:可以在Spring容器初始化完成之后,进行缓存等数据初始化,标准的写法如下:

有开发场景需要在启动系统过程中做一些初始化操作,比如定时任务初始化,请统一使用SystemInitializer机制

public class DemoManagerImpl extends AbstractSystemInitializer implements DemoManager {
    // 经典开头:养成记录日志的好习惯
    private Log LOGGER = CtpLogFactory.getLog(DemoManagerImpl.class);
    // 使用注解标准注入Bean
    @Inject
    private OrgManager orgManager;
    @Inject
    private IndexApi indexApi;
    // 重写AbstractSystemInitializer下的initialize方法,该方法的执行时间是:系统所有Spring初始化完成之后,根据平台默认任务编排依次执行继承了SystemInitializer的initialize方法
    @Override
    public void initialize() {
        // 此时所有Bean已经初始化完成,可以放心调用所有Bean
        orgManager.xxx
    }
编撰人:het、lichaoj