前言#

背景#

“变化”的挑战#

在软件工程领域有一句名言:“一个软件的生命周期10%的时间是开发,90%的时间在维护”。软件的研发可以通过“项目”的方式来进行,开发团队只需聚焦一个相对固定的目标,设定好开发计划,然后执行之。而后期的维护,往往具备更多的"变化",这些变化造成的挑战,绝对不小于前期的开发过程。随着互联网的推广,后期的维护时间仍在延长。在此时间内,软件的维护人员需要针对系统运行中产生的Bug及新需求为原来的程序打上一个个的补丁,随着持续的变化及人员流动,最终造成代码间网状的相互关联,出资人不得不重新投资开发全新的"2.0"版本。这中间的过渡,往往给用户体验带来巨大的影响。

微服务架构的困惑#

软件架构中"微服务"理念的提出,很好的解决上面的问题。但是微服务的落地,却给开发者带来了不小的尴尬,其主要的难点在于时机。由于微服务的治理给前期软件开发增加了诸多复杂度,主要包含了版本管理策略、发布管理、持续集成、依赖管理、自动部署等一系列要素,而这些要素往往是与业务无关的。新项目或初创团队,往往难以花过多的精力聚焦到前期的服务化运维或者后期的架构变更规划中,而当后期真正遇到功能或性能扩展瓶颈时,又被历史数据或关联影响所束缚,难以快速将微服务架构实施落地。

Maven的局限#

Maven是一个包管理工具,通过重新定义的工程与产出物,实现了软件的模块化及依赖管理。但Maven关注的是“编译器”的模块化,无法解决“运行时”的模块化,因此应用了Maven的项目仍然难以实现如下特性:
  1. 容忍局部失败:一旦整个应用的某个部分启动失败,则整个应用均不可用。
  2. 动态增删模块:运行过程中, 增加或删除模块、甚至配置项的改动均需要重启服务。

Strato简介#

Strato开发框架是一套微内核、模块化框架,提出并完整实现了“模块及服务”的理念,前期帮助开发团队快速开发出设计优良的产品,后期可直接将模块拆分成服务,帮助应用架构实现无缝的垂直切分及水平扩展。

微内核与模块化#

百度词条中如是定义:“把操作系统中更多的成分和功能放到更高的层次(即用户模式)中去运行,而留下一个尽量小的内核,用它来完成操作系统最基本的核心功能,称这种技术为微内核(Micro Kernel)技术。” 微内核的主要优势,是为系统提供了一个稳定的运行基础,无论上层的功能是否可用,都不会影响到底层的运行,只要底层持续可用,那么就有机会恢复上层的功能。因此,底层内核越微小,则系统越稳定。 微内核往往是和模块化相辅相成的,底层采用微内核,那么上层必定实现了模块化。

Java中的模块化技术#

Java语言有两大特性,一是Java是一种面向对象语言,同时其语法内置的包和类的概念;二是Java是一种编译型语言,其编译产出物通常以jar文件的形式发布。这两个特性已经提供了最基本的模块化功能,使得Java在编程语言领域占据了大量的市场。

而另一方面,正是因为Java生成的class不可修改,或者说修改成本很高,导致使用Java语言开发的软件,其应对变化的能力很弱,为了解决这个问题,杰出的Java程序员们采用了很多方法和技巧,而这些方法和技巧的核心思想,都是“模块化”。

目前常见的Java模块化技术,主要包括:JDK SPI,Jigsaw和OSGi。

OSGi#

OSGi是基于Java语言的一套模块化开发协议,其定义了模块的定义及模块的依赖、模块的生命周期、包的管理及依赖、运行时对象的管理及生命周期等规范,借助Java的ClassLoader特性,提供包和运行对象间的隔离机制,实现了模块的可插拔性。常见的OSGi规范的实现有Eclipse的Equinox和Apache的Felix,考虑到Equinox在OSGi上进行很多非标准化的扩展,Strato框架全面采用Felix实现OSGi。

模块即服务#

关于“模块化”的定义,从概念上可以表现为“聚合”、“依赖”和“隔离”。“聚合”指将完成相同任务的代码和资源归为相同的模块;“依赖”指模块间存在逻辑的关联关系及顺序;“隔离”指模块只需对外暴露其提供的服务抽象,而无需暴露具体实现。 而对于Java语言,模块化又体现在了“源码”、“编译期”和“运行时”三个方面。可以参看下表:

源码编译期运行时
聚合 工程、目录 Jar包 -
依赖 - Import对象关联
隔离 工程、目录 面向接口编程接口引用

综上可见,“模块化”很好地满足了“高内聚,低耦合”的特点。 事实上,Strato内置了阿里Dubbo服务的支持,结合Strato提出的模块划分方法论,可以轻易地实现模块化->微服务化的过渡

模块的管理#

已经有很多开发语言和工具实现了模块的管理,譬如:nodejs的npm、centos的yum、java的maven、python的pip等等。总的来说,模块的管理包含类似的组件,下表以maven为例,对比的Strato的模块化支持程度。

运行时支持模块、版本定义模块仓库下载/更新命令持续集成
Maven Spring pom.xml Nexus mvn 支持Jenkins
Strato Felix(OSGi) manifest.mf 内置支持 ant+build.xml 支持Jenkins

一个简单的例子#

下面的例子通过简单用户管理功能,展示了Strato在模块化的设计理念下,各模块间如何协同组成一个完整的系统。

Demo下载#

本例的工程安装程序可到这里下载。

运行Demo#

根据需要,Demo可以以开发和生产两种方式运行。
  • 开发模式
1. 下载AllInOne,再将上面的Demo源码解压至AllInOne/eclipse-workspace/strato_demo目录
2. 启动Eclipse,以AllInOne/eclipse-workspace作为工作空间,并导入AllInOne/eclipse-workspace下的所有工程(包括webapp工程)。
3. Eclipse打开Ant视图,运行build.xml下的jar-all打包
4. 在Mysql中建立数据库strato_demo,并执行以下DDL语句:
CREATE TABLE `t_user` (
  `user_id` int(36) NOT NULL COMMENT '人员ID',
  `name` varchar(100) DEFAULT NULL COMMENT '姓名',
  `sex` varchar(10) DEFAULT NULL COMMENT '性别',
  `birthday` datetime DEFAULT NULL COMMENT '出生日期',
  `phone` varchar(50) DEFAULT NULL COMMENT '手机号',
  `phone_verified` varchar(1) DEFAULT NULL COMMENT '手机是否验证',
  `email` varchar(40) DEFAULT NULL COMMENT '邮箱',
  `email_verified` varchar(1) DEFAULT NULL COMMENT '邮箱是否验证',
  `photo` varchar(200) DEFAULT NULL COMMENT '头像',
  `signature` varchar(500) DEFAULT NULL COMMENT '个性签名',
  PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='人员表';
5. 修改webapp/WEB-INF/src/ROOT.properties,增加以下配置项:
database.url=jdbc:mysql://localhost/strato_admin?useUnicode=true&characterEncoding=UTF-8
database.username=user
database.password=pass
database.maxActive=10
6. 运行AllInOne/bin/startup.bat启动服务
7. 浏览器访问http://localhost:8080/demo/user/list
  • 生产模式
1. 配置Ant运行环境,并下载ops工具,解压到任意目录
2. 修改build.properties,增加StratoDemo站点
#Strato框架版本
strato.framework.version=4.1.1
#Strato下载网站地址
strato.download.baseurl=http://download.stratoio.com/framework


#以下为site配置,实际应用可借助jenkins覆盖
sites.list=com.stratoio,StratoDemo
site.com.stratoio.url=http://repo.stratoio.com/nightly/4.1.1
site.StratoDemo.url=http://repo.zim-soft.com/StratoDemo

3. 运行ant create初始化运行目录
4. 运行ant select选择插件
5. 运行ant update下载插件
6. 参照开发模式的第4,5部初始化数据库配置
7. 运行ant start启动服务

核心功能#

工程结构#

关于工程结构的介绍,参看关于OSGiEclipse开发环境搭建

Ops部署工具#

关于Ops部署工具,参看"Ops服务器部署"。

服务管理#

OSGi常用对象#

OSGi的好处之一,就是通过ClassLoader技术,对运行时对象和类进行,对象和对象之前只需通过接口定义依赖,而不关注具体实现,真正实现了面向接口的开发,从而达到可插拔的目的。 下面就OSGi中最常用的对象做简单介绍,以便于读者了解其运行机制。 众所周知,jar文件是Java开发中的一个发布单元,而Bundle赋予了jar包动态特性,使其具有了一定的生命周期。下图呈现了Bundle的完整状态变化。
Strato采用Apache Felix作为OSGi的实现,在内核中,Felix永远将自己注册成ID为0的Bundle。 顾名思义,BundleContext是Bundle的上下文信息。 如果说Bundle类描述了一个Bundle的自然特性,那么BundleContext则管理着Bundle在运行过程中的动态属性。通过BundleContext可以管理服务的注册和Bundle间的属性共享。 BundleActivator是Bundle生命周期的简单回调,当Bundle启动或停止时出发。下面给出BundleActivator的两个使用场景:
      • 获得BundleContext
在Bundle内部定义类Activator
//监听Bundle启停并管理BundleContext引用
public class Activator implements BundleActivator {
	private static BundleContext bundleContext;
	
	@Override
	public void start(BundleContext context) throws Exception {
		bundleContext = context;
	}

	
	@Override
	public void stop(BundleContext context) throws Exception {
		context = null;
	}
	
	public static BundleContext getContext(){
		return bundleContext;
	}
}
此时Bundle内的其他对象则可通过下列方式获得当前Bundle的BundleContext引用。
BundleContext context=Activator.getContext()

      • 初始化资源
例如,下面的代码是一个典型的Activator,初始化连接、事务和文件服务。
//Bundle启动时初始化运行环境和相关资源
public class Activator implements BundleActivator{

	private TransactionInterceptor txInterceptor;

	@Override
	public void start(BundleContext bundleContext) throws Exception {
		{
			DruidDataSource datasource=new DruidDataSource();
			datasource.setUrl(GlobalEnvironment.getInstance().resolveString("${database.url}"));
			datasource.setUsername(GlobalEnvironment.getInstance().resolveString("${database.username}"));
			datasource.setPassword(GlobalEnvironment.getInstance().resolveString("${database.password}"));
			datasource.setMaxActive(GlobalEnvironment.getInstance().resolveInteger("${database.maxActive}"));
			datasource.setDefaultAutoCommit(false);
			datasource.setFilters("stat");
			setDataSourceProxyFilters(datasource);
			datasource.init();
			DataSourceFactory.getInstance().setDataSource(DataSourceFactory.COMMON_DATASOURCE, datasource);
		}
		

		this.txInterceptor=new TransactionInterceptor();
		DataSourceTransactionManager txManager=new DataSourceTransactionManager();
		txInterceptor.setTransactionManager(txManager);
		BundleUtil.exportServices(bundleContext, txInterceptor, null, null);
		BundleUtil.exportServices(bundleContext, txManager, null, null);
		
		FileServer.createServer("demoFileServer", GlobalEnvironment.getInstance().resolveString("${demo.fileserver.dir}"));
	}

	@Override
	public void stop(BundleContext context) throws Exception {
		DruidDataSource datasource = (DruidDataSource) DataSourceFactory.getInstance().getDataSource(DataSourceFactory.COMMON_DATASOURCE);
		datasource.close();
		DataSourceFactory.getInstance().removeDataSource(DataSourceFactory.COMMON_DATASOURCE);
	}

	private void setDataSourceProxyFilters(DruidDataSource datasource) {
		List<Filter> proxyFilters = new ArrayList<Filter>();
		StatFilter statFilter = new StatFilter();
		statFilter.setMergeSql(true);
		statFilter.setLogSlowSql(true);
		statFilter.setSlowSqlMillis(10000);
		
		proxyFilters.add(statFilter);
	
		Log4jFilter log4jFilter = new Log4jFilter();
		proxyFilters.add(log4jFilter);
		
		datasource.setProxyFilters(proxyFilters);
		
		//JdbcDataSourceStat
	}

}

    • 定义服务
服务的定义即接口定义,而一个服务是接口的实现类实例化后的对象。Demo中对用户服务的定义如下:
//定义用户服务
public interface UserService {
	

	List<User> queryAllUser();

	int addUser(User user);
	
	List<User> selectUserById(int id);

	int removeUser(int id);

	User queryByPrimaryKey(int id);

	int modifyUser(User user);

	Pagination<User> queryUserByPagination(Pagination<User> pagination);
	
}
    • 注册服务
OSGi其本质上也可以理解成一个Ioc容器,下表对Spring和OSGi关于服务的注册做了一个对比。
Spring Bean BeanFactory
OSGi ServiceBundleContext
要注册服务首先要给出服务定义的实现类,下面的
//实现用户服务
public class UserServiceImpl implements UserService{
...
}
原生的OSGi API可以通过BundleContext注册服务。
//注册用户服务
UserServiceImpl userService=new UserServiceImpl();
Dictionary<String, String> dict=new Hashtable<>();
dict.put("id", "userService");
bundleContext.registerService(UserService.class, userService, dict);//dict为服务引用时的过滤条件

    • 查找服务
//获得服务引用
ServiceReference<UserService> ref=bundleContext.getServiceReference(UserService.class);
//获得服务实例
UserService userService=bundleContext.getService(ref);
可以到https://osgi.org/javadoc/r4v41/index.html查看OSGi的API文档

@OsgiService#

实际开发中,使用OSGi原生API注册和引用服务代码量大而繁琐。 为此,在Strato中,定义了@OsgiService注解,可以快速注册服务。
@OsgiService //<-注册服务
public class UserServiceImpl implements UserService{
...
}
对于容器外的对象,也可通过工具类com.strato.osgi.util.v1_0_0.BundleUtil注册服务

@OsgiWired#

服务的引用同样简单,譬如:
@Controller
public class UserController{

    @OsgiWired //<-引用UserService服务
    private UserService userService;
    
}
@OsgiService和@OsgiWired是一对注解,在Strato中类似的注解还有两对:@DubboService和@DubboReference,以及@RestfulService和@RestfulReference。开发人员可以根据具体应用场景选择特定的服务注册,也可以混合使用。对于单个部署节点来说,多个注解将共享一个服务实例。因此,服务的实例应该确保是无状态的。

配置项管理#

配置项表达式#

Strato内置的注解,在任何时候,都支持使用${a.b.c}的动态格式替换常量值,例如:
//Example1:替换注册的URL
@Controller
@RequestMapping
@RequestMapping("${my.controller.url.prefix}")
public class MyController{

    @RequestMapping("${my.controller.url.foo}")
    public void foo(){}
    
}
//Example2:替换引用的Service
@OsgiWired(query="(id=${user.service.id})")
private UserService userService;
//Example3:替换权限
@SecurityConstrant(${auth.bar.resource})
@RequestMapping("/bar")
public void bar(){
}
//Example4:替换数据源名称
@MybatisWired(datasource="${user.datasource}")
private UserMapper userMapper;

Strato提供了一套灵活的机制来实现以上配置项的取值,最简单的办法是修改类路径下的ROOT.properties(对于Tomcat是lib/ROOT.properties,对于Eclipse是webapp/WEB-INF/src/ROOT.properties)。 针对以上的例子,给出以下配置示例:

#ROOT.properties

#Example1
my.controller.url.prefix=/my
my.controller.url.foo=/foo

#Example2
user.service.id=anotherUserService

#Example3
auth.bar.resource=bar

#Example4
user.datasource=defaultDS

配置项之间可以嵌套引用,譬如对于Passport的URL:

strato.baseurl=http://localhost:8080
strato.passport.urlPreffix=/passport

strato.passport.fullUrl=${strato.baseurl}${strato.passport.urlPreffix}    #http://localhost:8080/passport

@ConfigItem#

随着系统的规模增加,配置项的个数也会增加,而配置项是集中式的,存在着一定的管理风险。一方面,经过3-5年的系统维护,可能会有些配置项已经被废弃了,但是生产环境下不敢删除;或者某个开发人员新增了一个配置项,导致其他开发人员启动对应的模块失败。 Strato的设计目标,有一项就是令配置,开箱即用。因此,我们定义了@ConfigItem注解,来描述一个配置项。以下是@ConfigItem的定义:
@Target({ElementType.FIELD,ElementType.METHOD,ElementType.TYPE,ElementType.PARAMETER,ElementType.LOCAL_VARIABLE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ConfigItem {

	public static enum Type{String,Double,Int,Boolean};
	
	public static String EMPTY="";
	
	/**
	 * 配置项名称,不含${}。例如,${a},那么配置项名称为a
	 * @return
	 */
	String name();
	
	/**
	 * @return 配置项值类型
	 */
	Type type() default Type.String;
	
	boolean evaluate() default true;
	
	/**
	 * @return 参数用途(备注)
	 */
	String comment() default "";
	
	/**
	 * @return 默认值
	 */
	String defaultValue() default EMPTY;
}

下面为

@Controller
@ConfigItem(name="strato.demo.url.prefix",defaultValue="/strato/demo",comment="Demo的URL前缀")
@RequestMapping("${strato.demo.url.prefix}")
public class HomeController {

	@RequestMapping("/")
	public String home(){
		return index();
	}
	
	@RequestMapping("/index")
	public String index(){
		return "index";
	}
	
	...
	
}
借助StratoAdmin管理套件,可以很方便地查看某个配置项的引用详情和部署节点的配置全貌。

层级化配置#

通常对于单节点的部署只需要一个properties文件即可满足要求,但在集群环境下可能会有更加集中化的配置项管理需求。事实上Strato提供了一套多级可重载的配置框架。 默认AllInOne已经提供了三级配置:SYSTEM,INITIALS,RUNTIME。
SYSTEM
系统变量及Servlet上下文参数。
INITIALS
ROOT.properties配置项
RUNTIME
@ConfigItem中定义的defaultValue或者GlobalEnvironment.set(k,v)改变的配置项

多级配置对于重复定义的属性遵从优先级原则,对于默认配置来说,覆盖顺序为SYSTEM<-INITIALS<-RUNTIME。例如,当启动时jvm参数定义-Da.b.c=1,在ROOT.properties定义a.b.c=2,此时INITIALS级别生效,${a.b.c}为2。接着在运行时调用GlobalEnvironment.getInstance().set("a.b.c","3"),此时RUNTIME级别生效,${a.b.c}为3。

开发者可以通过修改ROOT.properties,增加新的配置加载方式,配置方式类似于log4j.properties。例如,

...

strato.env.loaders=custom,jdbc

strato.env.loader.custom.class=com.strato.base.env.v1_0_0.config.loader.ClassPathResourceLoader
strato.env.loader.custom.resource=custom.properties

strato.env.loader.jdbc.class=com.strato.base.env.v1_0_0.config.loader.JdbcLoader
strato.env.loader.jdbc.driverClass=com.mysql.jdbc.Driver
strato.env.loader.jdbc.url=jdbc:mysql://localhost/config
strato.env.loader.jdbc.username=user
strato.env.loader.jdbc.password=pass

...

此时框架会把原来SYSTEM、INITIALS、RUNTIME追加到新增的custom和jdbc前后,此时的覆盖顺序变为:SYSTEM<-INITIALS<-custom<-jdbc<-RUNTIME。 开发者可以通过实现com.strato.base.env.v1_0_0.config.PropertiesLoader或者继承抽象类com.strato.base.env.v1_0_0.config.loader.AbstractPropertiesLoader来扩展新的加载器。

Strato也内置了一些实现类:

com.strato.base.env.v1_0_0.config.loader.ClassPathResorceLoader
从ClassPath加载资源文件
com.strato.base.env.v1_0_0.config.loader.InitialsLoader
即INITIALS,根据当前Servlet的ContextPath名称来加载属性文件,默认为ROOT.properties。
com.strato.base.env.v1_0_0.config.loader.SystemLoader
即SYSTEM,从System.getProperty()和servletContext.getInitialParameter()获得配置项。
com.strato.base.env.v1_0_0.config.loader.JdbcLoader
从数据库加载配置项。
com.strato.base.env.v1_0_0.config.loader.RuntimeLoader
运行时变化的参数,当程序执行GlobalEnvironment.getInstance().set(k,v)时变化。因为所有的配置都存储在内存中,因此停止后将丢失。

可以通过com.strato.base.env.v1_0_0.config.PropertiesStore获得特定的PropertiesLoader实例,例如

String loaderName="INITIALS";
PropertiesLoader loader = PropertiesStore.getInstance().getLoader(loaderName);
loader.setProperty("a.b.c", "4");
if(loader.editable()){
	loader.save();//持久化保存
}
    

日志管理#

自动注入Log#

Strato自主实现了日志的Facade,接口名称为com.strato.logging.v1_0_0.api.Log。并提供了log4j的默认实现。 Log支持@OsgiWired注入,例如
@Controller
public class Controllers{
    @OsgiWired
    private Log log;
    
    public void foo(){
        if(log.isDebugEnabled()){
            log.debug(...);
        }
    }
}

LogFactory#

对于一些不适用于自动注入的环境,如单例,可以通过LogFactory获得Log实例。例如,
Log log = LogFactory.getLog(getClass());

Logger扩展#

开发者可以通过实现com.strato.logging.v1_0_0.api.LogFactory接口并注册成服务来实现自定义Logger的目的。例如,Strato内置了基于SAAS的多商户日志工厂,代码摘要如下:
@OsgiService(id="saasLogFactory",usefor="saas")
public class SaasLogFactory implements LogFactory {

	@Override
	public Log getLogByName(String name) {
		Log syslog = LogFactory.getSystemLog(name);
		return new SaasLog(name,syslog);
	}

}
然后在使用端通过注入的方式引用:
public class HomeController {

	@OsgiWired(query="(usefor=saas)")
	private Log log;

        ...
}

Web开发#

Strato采用类似Spring MVC的基于注解的风格开发Web应用。支持@Controller,@RequestMapping,@PathVariable标签。

面向注解的MVC#

@Controller注解可以将任意对象注册成Controller。配合@RequestMapping和@PathVariable即可映射到特定的URL。下面是Demo中UserController的一个片段:
@Controller
@ConfigItem(name="strato.demo.url.prefix",defaultValue="/strato/demo",comment="Demo的URL前缀")
@RequestMapping("${strato.demo.url.prefix}/user")
public class UserController {

    ...
    
    @RequestMapping("/edit/{userId}")
    @SecurityConstraint(AuthConstants.USER_WRITE)
    public ModelAndView updateStaff(ModelAndView mav, @PathVariable int userId) {
        Map<Object, Object> modelMap = new HashMap<Object, Object>();
	User user = userService.queryByPrimaryKey(userId);
	modelMap.put("crumbs",CrumbsBuilder.create()
				.addCrumbs("用户管理", null).addCrumbs("用户编辑", null).getCrumbsArray());
	modelMap.put("user", user);
	mav.setModel(modelMap);
	mav.setViewName("editUser");
	return mav;
    }
    
    ...
}
注:
  1. {userId}为路径变量,对应参数列表中的@PathVariable int userId。
  2. ModelAndView包含了视图和模型,本例中视图名称为"editUser",模型包括“crumbs"和"user"

Strato对Controller方法传入的参数进行自动识别和注入,支持以下类型的注入:

HttpServletRequest
当前Http连接的request
HttpServletResponse
当前Http连接的Response
ModelAndView
自动创建一个ModelAndView
Bundle
所在bundle的Bundle对象
BundleContext
所在bundle的BundleContext对象

同时,自动识别如下类型的返回值类型:

Void
不返回值,此时开发人员对传入的ModelAndView设值,或者直接操作传入的HttpServletResponse处理响应。
String
返回视图名称
View
返回视图对象
ModelAndView
返回视图和模型信息

常见的方法格式有如下几种:

public void handle(HttpServletRequest request,HttpServletResponse response){
    ...
    response.getWriter().writer(...);
    response.flushBuffer();
}
public String handle(HttpServletRequest request,ModelAndView mav){
    ...
    mav.getModel().put(...,...);
    mav.getModel().put(...,...);
    return ...;
}
public void handle(HttpServletRequest request,ModelAndView mav){
    ...
    mav.getModel().put(...,...);
    mav.getModel().put(...,...);
    mav.setViewName(...);
    
}
public View handle(HttpServletRequest request,ModelAndView mav){
    ...
    mav.getModel().put(...,...);
    mav.getModel().put(...,...);
    return new MyView();
}
public ModelAndView handle(HttpServletRequest request){
    ...
    ModelAndView mav=new ModelAndView();
    mav.setViewName(...);
    mav.getModel().put(...,...);
    mav.getModel().put(...,...);
    return mav;
}

视图管理#

视图名称#

Strato MVC定义视图格式为:[viewType:]viewName[@bundleSymbolicName]。其中,[]内的格式为可选格式,viewName为必选。
  • viewType 视图类型,默认为vm。目前支持vm和vml。当需要指向vml视图,且vm有同名视图时,可明确声明viewType为vml。
  • viewName viewName即视图名称,支持大小写字母、数字和特殊字符$_-/,不允许使用特殊字符:@。
  • bundleSymbolicName view所在Bundle的名称,默认为Controller所在Bundle。若指明则取对应Bundle下的对应viewName。常用于跨Bundle的视图引用。

以下是一些视图名称的例子:


vml:layout.root@demo.layout.v1_0_0

velocity/user

vm:velocity/user

vm:velocity/user@demo.user.web.v1_0_0

velocity/user@demo.user.web.v1_0_0

Velocity视图#

Strato内置Velocity作为视图名称。支持Velocity的完整语法。 Velocity视图支持多级目录查找,例如当视图名称为velocity/user/form时,将定位到当前Bundle所在Jar包的/velocity/user/form.vm。

VML视图#

Velocity提供#include和#parse宏,支持简单的视图包含,但对于复杂的场景,如视图继承、局部重载等特性,则无法支持。因此,Strato吸收了Apache Tiles设计理念,实现了vml视图。 VML视图定义在每个META-INF/layout.vml文件中,框架在Bundle启动时会自动扫描该文件,并提取其中的vml定义注册到视图解析器中。下面是Demo的布局定义:

<!-- strato.demo.layout.v1_0_0/META-INF/layout.vml -->
<?xml version="1.0" encoding="UTF-8"?>
<definitions>
	<definition name="rootLayout" page="velocity/layout">
		<put name="header" value="velocity/header" type="page"/>
		<put name="crumbs" value="velocity/crumbs" type="page"/>
		<put name="body" value="" type="page"/>
		<put name="foot" value="velocity/foot" type="page"/>
		<put name="title" value="" type="string"/>
	</definition>
	<definition name="paginationRootLayout" extend="vml:rootLayout">
		<put name="pagination" value="velocity/pagination" type="page"/>
	</definition>
</definitions>
以下是用户列表页的布局定义:
<!-- strato.demo.user.web.v1_0_0/META-INF/layout.vml -->
<?xml version="1.0" encoding="UTF-8"?>
<definitions>
	<definition name="listUser" extend="vml:paginationRootLayout@strato.demo.layout.v1_0_0">
		<put name="body" value="velocity/listUser" type="page"/>
		<put name="title" value="用户列表" type="string"/>
	</definition>
	<definition name="addUser" extend="vml:rootLayout@strato.demo.layout.v1_0_0">
		<put name="body" value="velocity/addUser" type="page"/>
		<put name="title" value="添加用户" type="string"/>
	</definition>
	<definition name="editUser" extend="vml:rootLayout@strato.demo.layout.v1_0_0">
		<put name="body" value="velocity/editUser" type="page"/>
		<put name="title" value="编辑用户" type="string"/>
	</definition>

</definitions>

视图扩展#

开发者可以通过实现com.strato.mvc.api.v1_0_0.view.ViewResolver并注册成服务来扩展视图。ViewResolver定义如下:
public interface ViewResolver extends Ordered{

	public View resolveView(ViewName viewName,Locale locale);
	
	public String getName();
	
	public static ViewName parseViewName(String viewName){
		return ViewName.parse(viewName);
	}
	
}
View定义如下:
public interface View {

	public void render(HttpServletRequest request,HttpServletResponse response,Map<?,?> model);
	
}

对象绑定#

静态资源#

Strato的核心理念是与功能相关的所有资源,因此对于传统的MVC架构来说,我们也希望对于性能要求不高系统,能够将像css、js这样的资源也放入到Bundle中,而不是放到系统外部(对于性能要求高的系统仍然可以放到系统外部,如CDN)。对于包内的静态资源,可以通过StaticResource静态工具来导出。以Demo为例,工程结构如下:
可以在包内开发一个Controller,将静态资源映射到外部。
@Controller
@ConfigItem(name="strato.demo.url.prefix",defaultValue="/strato/demo",comment="Demo的URL前缀")
@RequestMapping("${strato.demo.url.prefix}")
public class ResourceController {
	@RequestMapping("/css/**")
	public void css(HttpServletRequest request,HttpServletResponse response,Bundle bundle){
		StaticResource.delegateToWebApp(request, response, bundle,GlobalEnvironment.getInstance().resolveString("${strato.demo.url.prefix}"));
	}
	
	@RequestMapping("/js/**")
	public void js(HttpServletRequest request,HttpServletResponse response,Bundle bundle){
		StaticResource.delegateToWebApp(request, response, bundle,GlobalEnvironment.getInstance().resolveString("${strato.demo.url.prefix}"));
	}
	
	
	@RequestMapping("/images/**")
	public void images(HttpServletRequest request,HttpServletResponse response,Bundle bundle){
		StaticResource.delegateToWebApp(request, response, bundle,GlobalEnvironment.getInstance().resolveString("${strato.demo.url.prefix}"));
	}
	
	@RequestMapping("/fonts/**")
	public void fonts(HttpServletRequest request,HttpServletResponse response,Bundle bundle){
		StaticResource.delegateToWebApp(request,response, bundle,GlobalEnvironment.getInstance().resolveString("${strato.demo.url.prefix}"));
	}
	
	@RequestMapping("/fontawesome/**")
	public void font_awesome(HttpServletRequest request,HttpServletResponse response,Bundle bundle){
		StaticResource.delegateToWebApp(request, response, bundle,GlobalEnvironment.getInstance().resolveString("${strato.demo.url.prefix}"));
	}
}

StaticResource对Http缓存做了简单的优化,主要影响到以下Http Header:

Cache-Control
public,max-age=3600
Last-Modified
取Bundle的启动时间
ETag
Bundle的启动时间字符串
Expires
3600000(1小时)

权限拦截器#

Strato内置了权限拦截器,开发者可以在方法上使用注解@com.strato.mvc.security.v1_0_0.interceptor.SecurityConstraintInterceptor来设置访问权限, 例如:
@RequestMapping("/add")
@SecurityConstraint("user_write") //<-拦截权限
public ModelAndView addUser(ModelAndView mav, User user) {
	Map<Object, Object> modelMap = new HashMap<Object, Object>();
	modelMap.put("crumbs",CrumbsBuilder.create()
			.addCrumbs("用户管理", null).addCrumbs("添加用户", null).getCrumbsArray());
	mav.setModel(modelMap);
	mav.setViewName("addUser");
        return mav;
}

@SecurityConstraint用法:@SecurityConstraint("res1,res2,res3,res4") 其中,res1、res2、res3、res4代表该方法对应的url所对应的资源权限,注意此处并没有假定res1-res4是“且”的关系还是“或”的关系。对于用户是否有权限,由另一个接口“com.strato.mvc.security.v1_0_0.api.AuthorityService”来定义。 AuthorityService的定义如下:
public interface AuthorityService {

	public boolean check(Principal principal,String authorities);
	
	/**
	 * @param principal
	 * @param authorities
	 * @return 该principal是否具有authorities权限
	 */
	public boolean check(Principal principal,String[] authorities);

	public boolean check(String opers);
	
	public boolean check(String[] opers);

	/**
	 * 当无权限时的处理动作,如返回403响应
	 * @param request
	 * @param response
	 * @param mav
	 */
	public void onUnauthorized(HttpServletRequest request,HttpServletResponse response, ModelAndView mav);
	
}
AuthorityService的核心方法是public boolean check(Principal principal,String authorities),第一个参数是用户身份信息,第二个参数是@SecurityConstraint中定义的权限列表,由该接口的实现类来决定从何处查询用户的权限,以及多个authorities之间是“且”还是“或”的关系。 注意实习类必须注册为OSGi服务,即加上@OsgiService注解。

菜单管理#

Strato提供了@com.strato.mvc.menu.v1_0_0.annotation.MenuItem注解来描述一个菜单项,MenuItem的定义如下:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MenuItem {

	public static final String AUTO_DETECT_URL="auto";
	public static final String DEFAULT_GROUP="default";
	
	/**
	 * 菜单项所在分组
	 * @return
	 */
	public String group() default DEFAULT_GROUP;
	
	/**
	 * 菜单项的层级路径
	 * @return
	 */
	public String path();
	
	/**
	 * 显示文本
	 * @return
	 */
	public String text();
	
	/**
	 * 图表(可以是css样式也可以是图片url,此处只是呈现信息,具体以渲染时为准)
	 * @return
	 */
	public String icon() default "";
	
	/**
	 * 菜单项的url
	 * @return
	 */
	public String url() default AUTO_DETECT_URL;
	
	/**
	 * 在同一层级的显示顺序
	 * @return
	 */
	public int order() default 100;
	
	/**
	 * 当url为相对路径是,是否包含了contextPath
	 * @return
	 */
	public boolean includeContext() default false;
}

以demo为例,演示菜单项的使用:

@RequestMapping("/list")
@SecurityConstraint(AuthConstants.USER_READ)
@MenuItem(group="strato.demo",path="/user/list",text="用户列表")  //当url未定义时,取@RequestMapping的实际值
public ModelAndView list(HttpServletRequest req, HttpServletResponse res, ModelAndView mav, User user) {
    ...
}
Demo用户管理根菜单下有两个子菜单项,分别定义为:
@MenuItem(group="strato.demo",path="/user",text="用户管理")    //根菜单
@MenuItem(group="strato.demo",path="/user/list",text="用户列表",url="/user/list")    //查询子菜单
@MenuItem(group="strato.demo",path="/user/add",text="新增用户",url="/user/add")    //新增子菜单

框架在扫描并启动Bundle后,会将发现的所有@MenuItem注解通过MenuStore实例注册为一颗以com.strato.mvc.menu.v1_0_0.Menu为节点的树。 在视图如velocity中,可以通过$menuStore获取对应的Menu节点,然后按照特定的呈现方式生成菜单。 以StratoAdmin为例,header.vm中以如下方式生成菜单:

<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
	<ul class="nav navbar-nav">
		#set($menu=$menuStore.load("strato.admin","/"))
		#foreach($sub in $menu.getSortedSubMenus())
			#set($subsubs=$sub.getSortedSubMenus())
			#set($url=$sub.getFullUrl($request.contextPath))
			#if($subsubs)
			<li class="dropdown">
				<a #if($url) href="$url" #end class="dropdown-toggle" data-toggle="dropdown" role="button"  aria-expanded="false">$sub.text<span class="caret"></span></a>
				<ul class="dropdown-menu" role="menu">
					#foreach($subsub in $subsubs)
					<li><a href="$subsub.getFullUrl($request.contextPath)">$subsub.text</a></li>
					#end
				</ul>
			</li>
			#else
			<li><a href="$sub.getFullUrl($request.contextPath)">$sub.text</a></li>
			#end
		#end
	</ul>
</div>
注意@MenuItem必须定义在方法上,因此对于父菜单项,可能没有具体的业务url对应,此时可以创建一个空方法来挂载父菜单的@MenuItem。

面包屑#

和@MenuItem注解一样,框架也提供了一套生成面包屑数据的API。应用在每个Controller方法中生成面包屑并放置在ModelAndView中返回给视图,再视图中自行渲染。 下面摘一段Demo中的代码示例,
public ModelAndView list(HttpServletRequest req, HttpServletResponse res, ModelAndView mav, User user) {
	...
	mav.getModel().put("crumbs",CrumbsBuilder.create().addCrumbs("用户管理", null).addCrumbs("用户列表",GlobalEnvironment.getInstance().resolveString("${strato.demo.url.prefix}/user/list")).getCrumbsArray());
	...
}

在视图层的vml可以在页面头部插入crumbs.vm,内容如下:

#if($crumbs)
<div class="breadcrumb-nav">
	<div class="container">
		<ol class="breadcrumb">
			#foreach($crumb in $crumbs)
			#if($crumb.url)
				<li><a href="$crumb.url">$crumb.label</a></li>
			#else
				<li class="active">$crumb.label</li>
			#end
			#end
		</ol>
	</div>
</div>
#end
效果如下

MVVM支持#

框架自4.1.0开始支持MVVM架构的客户端,详情可以参见Json API章节和OAuth2.0章节。

开发方法论#

模块的拆分#

接口与实现类隔离#

模型类隔离#

垂直切分#

水平切分#

工程的组织#

垂直切分#

”公共“模块#

Add new attachment

Only authorized users are allowed to upload new attachments.

List of attachments

Kind Attachment Name Size Version Date Modified Author Change note
jpg
bundle_state.jpg 13.3 kB 1 28-May-2017 14:40 Jarez
jpg
crumbs.jpg 9.3 kB 1 01-Jun-2017 01:51 Jarez
jpg
demo_static_resource.jpg 19.3 kB 1 30-May-2017 01:39 Jarez
« This page (revision-121) was last changed on 02-Jul-2017 17:55 by Jarez