工具_源代码生成器GGCode

简介

通过近期的努力,将编写的源代码生成器进行0.0.1版本的封版,代码生成器按数据表自动生成源代码包含简单业务的(增,删,改,查,分页等)。
部分集成的开源框架已通过配置文件进行开关配置,方便按业务进行开启和关闭。欢迎大家使用。
Github地址:https://github.com/stotem/GGCode

基础框架

源代码生成框架: rapid-framework

UI框架: sb-admin-2

代码框架: SpringMVC + mybatis + Velocity。

代码架构: 经典三层架构(MVC), 增加rpc模块做为调用三方api模块, 增加manager模块设置为缓存层与事务层。

主要转换规则:

  1. 数据表与数据列的注释做为UI展示title。
  2. 数据列的为空性、数据长度和是否可为空等属性转换为javax.validation的判断规则。
  3. 数据列的数据类型转换为实体对象属性的数据类型。

集成功能

  1. 增加shiro做为权限管理框架。
  2. 为文档数据存储添加MongoDB支持。
  3. 为缓存增加Redis支持, 且自动配置Spring Cache为Redis缓存。
  4. 以Velocity做为页面模板语言。
  5. 增加javax.validation为服务端验证框架。且Controller自动验证传入参数。
  6. 采用bonecp对数据库连接池化管理。
  7. spring的响应数据采用多视图配置。

如何使用?

在bin目录中通过配置generator.xml后直接java -jar GGCode-0.0.1.jar

generator.xml配置说明

详见generator.xml注释

数据库设计要求

为了规范生成的Code,针对数据表的设计,需要满足每张表包含以下字段,请使用者遵守:

1
2
3
4
data_id bigint PRIMARY KEY COMMENT '数据主键(与业务主键区分)', 
del_flag tinyint(1) DEFAULT 1 COMMENT '数据删除标识(1: 有效,2: 失效)',
create_time timestamp COMMENT '数据创建时间',
update_time timestamp COMMENT '数据最后update时间',

其它说明

  1. controller的访问URI不带后缀.
  2. 所有的页面文件后缀为view.

生成后程序截图


观点仅代表自己,期待你的留言。

Shiro与Cas集成后的登录流程时序图

框架介绍

Shiro是apache开源项目,主要用户客户端管理登录用户权限提示非常方式的认证接口及丰富的注解。
CAS ( Central Authentication Service )是Yale大学发起的一个企业级的、开源的项目,旨在为 Web 应用系统提供一种可靠的单点登录解决方案(属于Web SSO)。主要用于用户登录认证与登录有效期认证。

调用时序

集成cas与shiro后的用户登录时序图


观点仅代表自己,期待你的留言。

开源SSO项目cas认证返回更多的attribute信息

CAS项目介绍

介绍网址: https://github.com/apereo/cas
github地址:https://github.com/apereo/cas.git

默认情况下,当用户认证成功后,客户端代码中只能获取到用户的登录名称,不能获取到其它的信息(如:手机号,所属角色,已有授权等),好在提供了一个attribute的map可以进行其它信息的返回。

1
2
3
AttributePrincipal ap = AssertionHolder.getAssertion().getPrincipal();
String name = ap.getName();
Map<String,Object> attribute = ap.getAttributes();

增加attribute信息返回

修改ticket验证接口返回的xml信息

找到WEB-INF/view/jsp/protocol/2.0/casServiceValidationSuccess.jsp,在节点之前添加以下内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
<cas:authenticationSuccess>
//...原有信息..//
<c:if test="${fn:length(assertion.chainedAuthentications[fn:length(assertion.chainedAuthentications)-1].principal.attributes) > 0}">
<cas:attributes>
<c:forEach var="attr" items="${assertion.chainedAuthentications[fn:length(assertion.chainedAuthentications)-1].principal.attributes}">
<cas:attribute>
<cas:${fn:escapeXml(attr.key)}>${fn:escapeXml(attr.value)}</cas:${fn:escapeXml(attr.key)}>
</cas:attribute>
</c:forEach>
</cas:attributes>
</c:if>
</cas:authenticationSuccess>
</cas:serviceResponse>

在用户登录验证时增加attribute关联

在deployerConfigContext.xml配置文件中,默认采用AcceptUsersAuthenticationHandler类的users属性(以key为登录名value为密码)进行系统用户的设定。
而通过PersonDirectoryPrincipalResolver的attributeRepository属性进行attribute映射和配置。默认由NamedStubPersonAttributeDao类的backingMap属性进行key-value配置,针对所有的用户都生效。
提示:IPersonAttributeDao有很多的子类实现,可以通过源码查看各自实现的方式,我这里主要介绍json,xml和数据库三种方式实现。

json配置的方式

而在实际业务系统设计中针对不同的用户需要返回不同的attribute这个需求似乎不太适用,经过查看可能配置JsonBackedComplexStubPersonAttributeDao来在json中针对单个用户attribute进行配置。指定init-method为init方法
配置如下:

1
2
3
<bean id="attributeRepository" class="org.jasig.services.persondir.support.JsonBackedComplexStubPersonAttributeDao" init-method="init">
<constructor-arg index="0" value="classpath:users.json" />
</bean>

users.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"user1":{
"id":["1001"],
"firstName":["View"],
"lastName":["Jack"],
"roles":["view"],
"permissions":["support:*:view"]
},
"user2":{
"id":["1002"],
"firstName":["Admin"],
"lastName":["One"],
"roles":["admin"],
"permissions":["support"]
}
}

xml配置的方式

配置attributeRepository为XmlPersonAttributeDao来在xml中针对单个用户的attribute进行配置。
配置如下:

1
2
3
<bean id="attributeRepository" class="org.jasig.services.persondir.support.xml.XmlPersonAttributeDao">
<property name="mappedXmlResource" value="classpath:users.xml" />
</bean>

database读取的方式

配置attributeRepository为NamedParameterJdbcPersonAttributeDao来在数据库按用户名进行attribute的配置加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<bean id="attributeRepository" class="org.jasig.services.persondir.support.jdbc.SingleRowJdbcPersonAttributeDao">
<property name="dataSource" ref="datasource" />
<property name="sql" value="select email,name,username,password from cas_user where {0}" />
<!-- 组装sql用的查询条件属性 -->
<property name="queryAttributeMapping">
<map>
<!-- key必须是uername而且是小写否则会导致取不到用户的其它信息,value对应数据库用户名字段,系统会自己匹配 -->
<entry key="username" value="username" />
</map>
</property>
<property name="resultAttributeMapping">
<map>
<!-- key为对应的数据库字段名称,value为提供给客户端获取的属性名字,系统会自动填充值 -->
<entry key="username" value="username" />
<entry key="email" value="email" />
<entry key="name" value="name" />
<entry key="password" value="password" />
</map>
</property>
</bean>

密码加密方式配置

在实际开发中不能任由密码明文进行传输,一般来讲会进行加密。而cas可直接通过配置的方式进行加密方式的确定。
在配置的AcceptUsersAuthenticationHandler中继承了父类中的一个名为passwordEncoder的PasswordEncoder类型属性。通过给这个属性配置加密方式值就能实现。默认为PlainTextPasswordEncoder加密方式。
如配置md5的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
<bean id="primaryAuthenticationHandler" class="org.jasig.cas.authentication.AcceptUsersAuthenticationHandler">
<property name="passwordEncoder" ref="passwordEncoder" />
<property name="users">
<map>
<entry key="user1" value="123" />
<entry key="user2" value="123" />
</map>
</property>
</bean>
<bean id="passwordEncoder" class="org.jasig.cas.authentication.handler.DefaultPasswordEncoder">
<property name="characterEncoding" value="utf-8" />
<property name="encodingAlgorithm" value="MD5" />
</bean>

提示:配置了密码方式之后,所有配置文件或数据库中所存储的密码则应修改为密文。

重要发现

用户登录验证采用的AuthenticationHandler接口的子类实现,当我们自定义实现类完成验证时,不要妄想在通过HandlerResult的getPrincipal()获取到Principal然后给它设置attribute,因为cas在调用认证方式后会针对Principal的数据进行name的复制,所以即使你的认证方式进行了attribute的设置,在进行对象复制时也会丢掉。

详见:PolicyBasedAuthenticationManager的authenticateInternal方法中的逻辑。
PolicyBasedAuthenticationManager-authenticateInternal


观点仅代表自己,期待你的留言。

妙用Spring中的CompositeCacheManager

简介

Spring提供了@Cacheable,@CacheEvict,@CachePut等注解很方便有实现数据的缓存,而CompositeCacheManager主要用于集合多个CacheManager实例,在使用多种缓存容器时特别有用。

fallbackToNoOpCache属性的真实意义

经过查看Spring 4.1.9.RELEASE的源码,当CompositeCacheManager的fallbackToNoOpCache属性设置为true时,CompositeCacheManager会在已配置的cacheManagers末尾添加一个NoOpCacheManager。
当通过代码中指定的缓存容器(@Cacheable等注解设置的value)没有在cacheManagers中都找到时,则会进入到NoOpCacheManager中,此时就相当于禁用掉了缓存,而不抛出相应的异常。

网络上有朋友在说,当设置fallbackToNoOpCache属性设置为true时,则可以解决缓存容器没有准备好时自动禁用缓存的效果,经过查看Spring源码,并未实现。
不过,经测试以下方法可以实现通过配置禁用缓存。

通过配置禁用缓存

利用CompositeCacheManager + NoOpCacheManager还能解决当缓存容器没有准备好(缓存容器崩溃,网络不可用等)或者需要暂时去掉缓存的需求。

只需要将cacheManagers的list值的第一个元素设置为NoOpCacheManager就OK了。

spring.xml

1
2
3
4
5
6
7
8
9
<bean id="cacheManager" class="org.springframework.cache.support.CompositeCacheManager">
<property name="cacheManagers">
<list>
<bean class="org.springframework.cache.support.NoOpCacheManager" />
<ref bean="simpleCacheManager" />
</list>
</property>
<property name="fallbackToNoOpCache" value="true" />
</bean>


观点仅代表自己,期待你的留言。

类Google分页算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class Pagination {

@Test
public void getPagination() {
final int[] paginationRegion = getPaginationRegion(6, 5, 10);
// 进行页码输出
System.out.println(Arrays.toString(paginationRegion));
for (int i=paginationRegion[0]; i<= paginationRegion[1];i++) {
//....
}
}

/**
* 类Google分页 - 获取需要显示的闭区间页码范围
* @param currPage 当前页码
* @param showNum 显示条数,为保持当前页码居中,建议为单数值
* @param totalPage 总页数
* @return 闭区间 页码范围
*/
private static int[] getPaginationRegion(int currPage, int showNum, int totalPage) {
// 先进行基础运算
int startNum = currPage - showNum/2;
int endNum = currPage + showNum/2;
if (endNum > totalPage) {
endNum = totalPage;
}
if (startNum < 1) {
startNum = 1;
}
// 如果显示页码数量不够则进行补数
if(endNum - startNum < showNum) {
int tmp;
if ((tmp = endNum - showNum + 1) > 0) {
startNum = tmp;
}
else if((tmp = startNum + showNum - 1) < totalPage) {
endNum = tmp;
}
}
return new int[]{startNum, endNum};
}
}

注意:输入的为闭区间(包含两端页码值)范围。


观点仅代表自己,期待你的留言。

Spring-AOP实现之注解

AOP概念

AOP术语:

  • 切面(Aspect):一个关注点的模块化,这个关注点可能会横切多个对象。事务管理是J2EE应用中一个关于横切关注点的很好的例子。在Spring AOP中,切面可以使用基于模式或者基于@Aspect注解的方式来实现。
  • 连接点(Joinpoint):在程序执行过程中某个特定的点,比如某方法调用的时候或者处理异常的时候。在Spring AOP中,一个连接点总是表示一个方法的执行。
  • 通知(Advice):在切面的某个特定的连接点上执行的动作。其中包括了“around”、“before”和“after”等不同类型的通知(通知的类型将在后面部分进行讨论)。许多AOP框架(包括Spring)都是以拦截器做通知模型,并维护一个以连接点为中心的拦截器链。
  • 切入点(Pointcut):匹配连接点的断言。通知和一个切入点表达式关联,并在满足这个切入点的连接点上运行(例如,当执行某个特定名称的方法时)。切入点表达式如何和连接点匹配是AOP的核心:Spring缺省使用AspectJ切入点语法。
  • 引入(Introduction):用来给一个类型声明额外的方法或属性(也被称为连接类型声明(inter-type declaration))。Spring允许引入新的接口(以及一个对应的实现)到任何被代理的对象。例如,你可以使用引入来使一个bean实现IsModified接口,以便简化缓存机制。
  • 目标对象(Target Object): 被一个或者多个切面所通知的对象。也被称做被通知(advised)对象。 既然Spring AOP是通过运行时代理实现的,这个对象永远是一个被代理(proxied)对象。
  • AOP代理(AOP Proxy):AOP框架创建的对象,用来实现切面契约(例如通知方法执行等等)。在Spring中,AOP代理可以是JDK动态代理或者CGLIB代理。
  • 织入(Weaving):把切面连接到其它的应用程序类型或者对象上,并创建一个被通知的对象。这些可以在编译时(例如使用AspectJ编译器),类加载时和运行时完成。Spring和其他纯Java AOP框架一样,在运行时完成织入。

通知类型:

  • 前置通知(Before advice):在某连接点之前执行的通知,但这个通知不能阻止连接点之前的执行流程(除非它抛出一个异常)。
  • 后置通知(After returning advice):在某连接点正常完成后执行的通知:例如,一个方法没有抛出任何异常,正常返回。
  • 异常通知(After throwing advice):在方法抛出异常退出时执行的通知。
  • 最终通知(After (finally) advice):当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)。
  • 环绕通知(Around Advice):包围一个连接点的通知,如方法调用。这是最强大的一种通知类型。环绕通知可以在方法调用前后完成自定义的行为。它也会选择是否继续执行连接点或直接返回它自己的返回值或抛出异常来结束执行。

配置实现

实例需求: 监控各Api接口执行时间,找出耗时的业务操作。
1、启用@AspectJ支持
spring.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:util="http://www.springframework.org/schema/util" xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx" xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:cache="http://www.springframework.org/schema/cache" xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd
http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache-3.2.xsd"
default-lazy-init="true">

<aop:aspectj-autoproxy />

</beans>

2、增加aspectj依赖库
pom.xml

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.7.4</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.7.4</version>
</dependency>

3、编写切面类

1
2
3
4
5
6
7
8
9
10
11
12
13
@Aspect
@Repository
public class AspectPoint {

@Around("within(org.wujianjun.apps.service..*)")
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
final long beginTime = System.currentTimeMillis();
Object res = pjp.proceed(pjp.getArgs());
final long endTime = System.currentTimeMillis();
System.out.println(pjp.getTarget().getClass().getSimpleName()+"."+pjp.getSignature().getName()+"-->"+(endTime-beginTime)+"ms");
return res;
}
}

对比一下xml配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:util="http://www.springframework.org/schema/util" xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx" xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:cache="http://www.springframework.org/schema/cache" xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd
http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache-3.2.xsd"
default-lazy-init="true">

<bean id="aspectPoint" class="AspectPoint" />
<aop:aspectj-autoproxy/>
<aop:config>
<aop:aspect ref="aspectPoint">
<aop:around method="doBasicProfiling" pointcut="within(org.wujianjun.apps.service..*)" />
</aop:aspect>
</aop:config>

</beans>

附上测试业务实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package org.wujianjun.apps.service.impl;

@Service
public class ExampleServiceImpl implements ExampleService {
private final static Logger LOG = LoggerFactory
.getLogger(ExampleServiceImpl.class);

@Resource
private ExampleManager exampleManager;

@Override
public void test() {
System.out.println("test--");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

4、测试结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
六月 30, 2016 4:11:07 下午 org.apache.coyote.AbstractProtocol start
信息: Starting ProtocolHandler ["http-bio-8080"]
test--
ExampleServiceImpl.test-->103ms
test--
ExampleServiceImpl.test-->105ms
test--
ExampleServiceImpl.test-->103ms
test--
ExampleServiceImpl.test-->103ms
test--
ExampleServiceImpl.test-->100ms
test--
ExampleServiceImpl.test-->104ms

配置说明

AspectJ切入点指示符:

  • execution - 匹配方法执行的连接点,这是你将会用到的Spring的最主要的切入点指示符。
    具体格式:execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)throws-pattern?)
    示例: execution( com.xyz.service...*(..) - 在service包或其子包中定义的任意方法的执行
  • within - 限定匹配特定类型的连接点(在使用Spring AOP的时候,在匹配的类型中定义的方法的执行)。
    示例: within(com.xyz.service..*) - 在service包或其子包中的任意连接点(在Spring AOP中只是方法执行),包含类和接口
  • this - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中bean reference(Spring AOP 代理)是指定类型的实例。
    示例: this(com.xyz.service.AccountService) - 实现了AccountService接口的代理对象的任意连接点 (在Spring AOP中只是方法执行)
  • target - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中目标对象(被代理的应用对象)是指定类型的实例。
    示例: target(com.xyz.service.AccountService) - 实现AccountService接口的目标对象的任意连接点 (在Spring AOP中只是方法执行)
  • args - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中参数是指定类型的实例。
    示例: args(java.io.Serializable) - 任何一个只接受一个参数,并且运行时所传入的参数是Serializable 接口的连接点(在Spring AOP中只是方法执行)
  • bean - 标记可以是任何Spring bean的名字支持通配符’‘。
    示例: bean(
    Service) - 任何一个在名字匹配通配符表达式’*Service’的Spring bean之上的连接点 (在Spring AOP中只是方法执行)
  • @target - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中正执行对象的类持有指定类型的注解。
    示例: @target(org.springframework.transaction.annotation.Transactional) - 目标对象中有一个 @Transactional 注解的任意连接点 (在Spring AOP中只是方法执行)
  • @args - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中实际传入参数的运行时类型持有指定类型的注解。
    示例: @args(com.xyz.security.Classified) - 任何一个只接受一个参数,并且运行时所传入的参数类型具有@Classified 注解的连接点(在Spring AOP中只是方法执行)
  • @within - 限定匹配特定的连接点,其中连接点所在类型已指定注解(在使用Spring AOP的时候,所执行的方法所在类型已指定注解)。
    示例: @within(org.springframework.transaction.annotation.Transactional) - 任何一个目标对象声明的类型有一个 @Transactional 注解的连接点 (在Spring AOP中只是方法执行)
  • @annotation - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中连接点的主题持有指定的注解。
    示例: @annotation(org.springframework.transaction.annotation.Transactional) - 任何一个执行的方法有一个 @Transactional 注解的连接点 (在Spring AOP中只是方法执行)

注意:切入点表达式可以使用’&’, ‘||’ 和 ‘!’来组合

通知类型声明注解:

  • 前置通知 - 使用 @Before 注解声明
  • 后置通知 - 使用 @AfterReturning 注解来声明
  • 异常通知 - 使用@AfterThrowing注解
  • 最终通知 - 使用@After 注解来声明
  • 环绕通知 - 使用@Around注解来声明

共享通用切入点定义

通过@Pointcut先定义好切入点, 当通知类型可以通过被 @Pointcut 标识的方法名直接共享其切入点配置。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.xyz.someapp;

@Aspect
public class SystemArchitecture {
@Pointcut("within(com.xyz.someapp.web..*)")
public void inWebLayer() {}

@Before("com.xyz.someapp.SystemArchitecture.inWebLayer()")
public void doBefore() {

}

@After("com.xyz.someapp.SystemArchitecture.inWebLayer()")
public void doAfter() {

}
}

获取切入点更多的信息

任何通知方法可以将第一个参数定义为org.aspectj.lang.JoinPoint类型 (环绕通知需要定义第一个参数为ProceedingJoinPoint类型, 它是 JoinPoint 的一个子类)。JoinPoint 接口提供了一系列有用的方法,比如 getArgs()(返回方法参数)、 getThis()(返回代理对象)、getTarget()(返回目标)、 getSignature()(返回正在被通知的方法相关信息)和 toString() (打印出正在被通知的方法的有用信息)。


http://shouce.jb51.net/spring/aop.html

观点仅代表自己,期待你的留言。

Spring多视图配置及源码剖析

简介

在MVC架构中,Controller被用于连接Model与View,同时控制View的显示与跳转。在以往的开发实践中常常是直接将View与Controller放置在同一个Project中,如果此时想要真正实现Controller与View的分离,View与Controller的交互就只能通过HTTP+数据格式或者ajax实现。那么Controller所需要做的就是将数据通过api的方式提供给View。此时针对Controller就需要将数据进行api和view的两种完全不同的视图呈现。好在Spring已经给我们提供了org.springframework.web.servlet.view.ContentNegotiatingViewResolver来实现。

Spring多视图配置

spring-web.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<!--配置消息转换器-->
<mvc:annotation-driven>
<mvc:message-converters register-defaults="true">
<bean class="org.springframework.http.converter.StringHttpMessageConverter">
<constructor-arg value="UTF-8"/>
<property name="supportedMediaTypes">
<list>
<value>application/xml;charset=UTF-8</value>
<value>text/html;charset=UTF-8</value>
<value>text/plain;charset=UTF-8</value>
<value>application/json;charset=UTF-8</value>
</list>
</property>
</bean>
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
<property name="prettyPrint" value="true"/>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
<!--多视图解析配置-->
<bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
<property name="defaultContentType" value="text/html;charset=UTF-8" />
<!-- not by accept header -->
<property name="ignoreAcceptHeader" value="true"/>
<property name="favorPathExtension" value="true"/>
<property name="favorParameter" value="true"/>
<property name="useNotAcceptableStatusCode" value="true" />
<!-- by extension -->
<property name="mediaTypes">
<map>
<entry key="xml" value="application/xml" />
<entry key="json" value="application/json" />
<entry key="httl" value="text/html" />
</map>
</property>
<property name="viewResolvers">
<list>
<ref bean="httlViewResolver"/>
</list>
</property>
<property name="defaultViews">
<list>
<bean class="org.springframework.web.servlet.view.json.MappingJackson2JsonView" />
<bean class="org.springframework.web.servlet.view.xml.MappingJackson2XmlView" />
</list>
</property>
</bean>

<!-- 视图解析器配置 -->
<bean id="httlViewResolver" class="httl.web.springmvc.HttlViewResolver">
<property name="suffix" value=".view"/>
<property name="contentType" value="text/html;charset=UTF-8"/>
<property name="attributesMap" ref="viewTools" />
</bean>

Controller.java

1
2
3
4
5
6
7
8
9
10
@Controller
@RequestMapping("/")
public class CommonController {
@RequestMapping("index")
public ModelAndView indexPage() {
Map<String, Object> param = new HashMap<>();
param.put("_model_", new Example());
return getView("index", param);
}
}

以上为多视图配置的全部内容,配置中我们提供了三种视图。

  1. httl的页面视图。 通过访问/index或/index.httl就可以得到。
  2. json的数据视图。 通过访问/index.json就可以得到。
  3. xml的数据视图。 通过访问/index.xml就可以各到。

    这里需要注意的是,由于xml只有一个根节点,所以返回的param的Map中只能包含一个元素。如果配置多个map元素,则会抛出异常java.lang.IllegalStateException: Model contains more than one object to render, only one is supported

useNotAcceptableStatusCode:这个配置表示,当配置为true且不能找到你需要的配置时返回HttpStatus 406. 默认值为false

Spring源码剖析

首先当然是org.springframework.web.servlet.view.ContentNegotiatingViewResolver类,经过断点跟踪后,发现是通过resolveViewName()来定位的显示View。

从图上可以看出,先通过request获取到requestedMediaTypes。

获取请求的视图格式


再深入一层查看,发现这里是针对requestedMediaTypes的过滤,而获取请求格式有三个途径,
ServletPathExtensionContentNegotiationStrategy(继承自PathExtensionContentNegotiationStrategy),
ParameterContentNegotiationStrategy
FixedContentNegotiationStrategy
而这恰好对应了Spring中针对识别多视图标识的配置

1
2
3
<property name="ignoreAcceptHeader" value="true"/>
<property name="favorPathExtension" value="true"/>
<property name="favorParameter" value="true"/>

所以可以得出结论:
1、HeaderContentNegotiationStrategy对应ignoreAcceptHeader的配置,此处配置为false,那么ContentNegotiatingViewResolver.contentNegotiationManager属性里则会多出一个元素。通过其源码可以得到它是通过在request header中增加Accept消息头获取视图格式。
2、ServletPathExtensionContentNegotiationStrategy(或者PathExtensionContentNegotiationStrategy)对应favorPathExtension的配置。通过请求后缀获取视图格式。
3、ParameterContentNegotiationStrategy对应favorParameter的配置,通过在request uri中增加format=?获取视图格式。

按requestdMediaTypes获取到支持的ViewResolver

1
List candidateViews = this.getCandidateViews(viewName, locale, requestedMediaTypes);


这里所做的事,就是将controller处理完成的view名称拿到配置的ViewResolver里去查找(视图名称和加了后缀的视图名称两步搜索),最后再将配置的defaultViews直接加入到返回结果的List中。此处忽略掉了视图格式标识。

获取匹配度最高的View

1
View bestView = this.getBestView(candidateViews, requestedMediaTypes, attrs);

此方法所做的事:将包含视图名称的ViewResolver按requestedMediaTypes找到匹配度最高的View。
如果没有找到,则按useNotAcceptableStatusCode返回HttpCode 406.或者通过candidateViews的第一个进行视图渲染。这里的顺序可以通过配置viewResolvers的order属性确定

Velocity与Httl视图工具类实例处理方案

由于在页面视图中我们需要用到很多的工具类来对Controller返回的数据进行判断以及转换等操作,所以一般来讲我们会在Map存入工具类的实例对象以便能在Page中直接使用。
但是当返回的为xml视图时,由于元素数据限制为1个,所以这些工具类将会导致多视图不能返回xml的数据格式。

在Velocity中可以配置toolbox等工具组件来实现,但是Httl并未支持。所以我找到另一个实现方式。

经过源码的查看,UrlBasedViewResolver类中包含一个staticAttributes的Map类型属性,这里完全可以拿来存放工具类对象实例,staticAttributes属性的get和set方法名称为:setAttributesMap()和getAttributesMap(). 所以配置的时候要注意配置名为attributesMap
而HttlViewResolver与VelocityLayoutViewResolver都间接继承自UrlBasedViewResolver,所以可以通过配置attributesMap属性来实现工具类实例的配置。


观点仅代表自己,期待你的留言。

Maven打包source与javadoc

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>2.2.1</version>
<executions>
<execution>
<id>create-source</id><!--指定一个名字-->
<phase>compile</phase><!--在编译阶段生成source包-->
<goals>
<goal>jar</goal><!--指定生成的文件为jar包-->
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>2.9.1</version>
<executions>
<execution>
<id>create-doc</id><!--指定一个名字-->
<phase>compile</phase><!--在编译阶段生成javadoc包-->
<goals>
<goal>jar</goal><!--指定生成的文件为jar包-->
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

结论:由于指定插件在phase=compile时期生成源码与Javadoc,当执行完成编译后,会在target目录下生成三个文件,xxx.jar,xxx-javadoc.jar,xxx-sources.jar


观点仅代表自己,期待你的留言。

Zookeeper学习笔记

简介

Zookeeper 分布式服务框架是 Apache Hadoop 的一个子项目,它主要是用来解决分布式应用中经常遇到的一些数据管理问题,如:统一命名服务、状态同步服务、集群管理、分布式应用配置项的管理等。

Zookeeper的数据存储模型:ZK会维护一个具有层次关系的数据结构,它非常类似于一个标准的文件系统。
Zookeeper的数据存储模型

Zookeeper 数据结构有如下这些特点:

  1. 每个子目录项如 NameService 都被称作为 znode,这个 znode 是被它所在的路径唯一标识,如 Server1 这个 znode 的标识为 /NameService/Server1
  2. znode 可以有子节点目录,并且每个 znode 可以存储数据,注意 EPHEMERAL 类型的目录节点不能有子节点目录
  3. znode 是有版本的,每个 znode 中存储的数据可以有多个版本,也就是一个访问路径中可以存储多份数据
  4. znode 可以是临时节点,一旦创建这个 znode 的客户端与服务器失去联系,这个 znode 也将自动删除,Zookeeper 的客户端和服务器通信采用长连接方式,每个客户端和服务器通过心跳来保持连接,这个连接状态称为 session,如果 znode 是临时节点,这个 session 失效,znode 也就删除了
  5. znode 的目录名可以自动编号,如 App1 已经存在,再创建的话,将会自动命名为 App2
  6. znode 可以被监控,包括这个目录节点中存储的数据的修改,子节点目录的变化等,一旦变化可以通知设置监控的客户端,这个是 Zookeeper 的核心特性,Zookeeper 的很多功能都是基于这个特性实现的

Znode目录节点类型:

  1. PERSISTENT:持久化目录节点,这个目录节点存储的数据不会丢失;
  2. PERSISTENT_SEQUENTIAL:顺序自动编号的目录节点,这种目录节点会根据当前已近存在的节点数自动加 1,然后返回给客户端已经成功创建的目录节点名;
  3. EPHEMERAL:临时目录节点,一旦创建这个节点的客户端与服务器端口也就是 session 超时,这种节点会被自动删除;
  4. EPHEMERAL_SEQUENTIAL:临时自动编号节点

适用场景

统一命名服务(Name Service)

分布式应用中,通常需要有一套完整的命名规则,既能够产生唯一的名称又便于人识别和记住,通常情况下用树形的名称结构是一个理想的选择,树形的名称结构是一个有层次的目录结构,既对人友好又不会重复。说到这里你可能想到了 JNDI,没错 Zookeeper 的 Name Service 与 JNDI 能够完成的功能是差不多的,它们都是将有层次的目录结构关联到一定资源上,但是 Zookeeper 的 Name Service 更加是广泛意义上的关联,也许你并不需要将名称关联到特定资源上,你可能只需要一个不会重复名称,就像数据库中产生一个唯一的数字主键一样。
Name Service 已经是 Zookeeper 内置的功能,你只要调用 Zookeeper 的 API 就能实现。如调用 create 接口就可以很容易创建一个目录节点。

配置管理(Configuration Management)

配置的管理在分布式应用环境中很常见,例如同一个应用系统需要多台 PC Server 运行,但是它们运行的应用系统的某些配置项是相同的,如果要修改这些相同的配置项,那么就必须同时修改每台运行这个应用系统的 PC Server,这样非常麻烦而且容易出错。
像这样的配置信息完全可以交给 Zookeeper 来管理,将配置信息保存在 Zookeeper 的某个目录节点中,然后将所有需要修改的应用机器监控配置信息的状态,一旦配置信息发生变化,每台应用机器就会收到 Zookeeper 的通知,然后从 Zookeeper 获取新的配置信息应用到系统中。
配置管理结构图

集群管理(Group Membership)

Zookeeper 能够很容易的实现集群管理的功能,如有多台 Server 组成一个服务集群,那么必须要一个“总管”知道当前集群中每台机器的服务状态,一旦有机器不能提供服务,集群中其它集群必须知道,从而做出调整重新分配服务策略。同样当增加集群的服务能力时,就会增加一台或多台 Server,同样也必须让“总管”知道。
Zookeeper 不仅能够帮你维护当前的集群中机器的服务状态,而且能够帮你选出一个“总管”,让这个总管来管理集群,这就是 Zookeeper 的另一个功能 Leader Election。
它们的实现方式都是在 Zookeeper 上创建一个 EPHEMERAL 类型的目录节点,然后每个 Server 在它们创建目录节点的父目录节点上调用 getChildren(String path, boolean watch) 方法并设置 watch 为 true,由于是 EPHEMERAL 目录节点,当创建它的 Server 死去,这个目录节点也随之被删除,所以 Children 将会变化,这时 getChildren上的 Watch 将会被调用,所以其它 Server 就知道已经有某台 Server 死去了。新增 Server 也是同样的原理。
Zookeeper 如何实现 Leader Election,也就是选出一个 Master Server。和前面的一样每台 Server 创建一个 EPHEMERAL 目录节点,不同的是它还是一个 SEQUENTIAL 目录节点,所以它是个 EPHEMERAL_SEQUENTIAL 目录节点。之所以它是 EPHEMERAL_SEQUENTIAL 目录节点,是因为我们可以给每台 Server 编号,我们可以选择当前是最小编号的 Server 为 Master,假如这个最小编号的 Server 死去,由于是 EPHEMERAL 节点,死去的 Server 对应的节点也被删除,所以当前的节点列表中又出现一个最小编号的节点,我们就选择这个节点为当前 Master。这样就实现了动态选择 Master,避免了传统意义上单 Master 容易出现单点故障的问题。
集群管理结构图

共享锁(Locks)

共享锁在同一个进程中很容易实现,但是在跨进程或者在不同 Server 之间就不好实现了。Zookeeper 却很容易实现这个功能,实现方式也是需要获得锁的 Server 创建一个 EPHEMERAL_SEQUENTIAL 目录节点,然后调用 getChildren方法获取当前的目录节点列表中最小的目录节点是不是就是自己创建的目录节点,如果正是自己创建的,那么它就获得了这个锁,如果不是那么它就调用 exists(String path, boolean watch) 方法并监控 Zookeeper 上目录节点列表的变化,一直到自己创建的节点是列表中最小编号的目录节点,从而获得锁,释放锁很简单,只要删除前面它自己所创建的目录节点就行了。

队列管理

Zookeeper 可以处理两种类型的队列:

  1. 当一个队列的成员都聚齐时,这个队列才可用,否则一直等待所有成员到达,这种是同步队列。
  2. 队列按照 FIFO 方式进行入队和出队操作,例如实现生产者和消费者模型。
    同步队列用 Zookeeper 实现的实现思路如下:
    创建一个父目录 /synchronizing,每个成员都监控标志(Set Watch)位目录 /synchronizing/start 是否存在,然后每个成员都加入这个队列,加入队列的方式就是创建 /synchronizing/member_i 的临时目录节点,然后每个成员获取 / synchronizing 目录的所有目录节点,也就是 member_i。判断 i 的值是否已经是成员的个数,如果小于成员个数等待 /synchronizing/start 的出现,如果已经相等就创建 /synchronizing/start。

    集群配置

    zoo.cfg
    1
    2
    3
    4
    initLimit=5 
    syncLimit=2
    server.1=192.168.211.1:2888:3888
    server.2=192.168.211.2:2888:3888

说明:

  • initLimit:这个配置项是用来配置 Zookeeper 接受客户端(这里所说的客户端不是用户连接 Zookeeper 服务器的客户端,而是 Zookeeper 服务器集群中连接到 Leader 的 Follower 服务器)初始化连接时最长能忍受多少个心跳时间间隔数。当已经超过 10 个心跳的时间(也就是 tickTime)长度后 Zookeeper 服务器还没有收到客户端的返回信息,那么表明这个客户端连接失败。总的时间长度就是 5*2000=10 秒
  • syncLimit:这个配置项标识 Leader 与 Follower 之间发送消息,请求和应答时间长度,最长不能超过多少个 tickTime 的时间长度,总的时间长度就是 2*2000=4 秒
  • server.A=B:C:D:其中 A 是一个数字,表示这个是第几号服务器;B 是这个服务器的 ip 地址;C 表示的是这个服务器与集群中的 Leader 服务器交换信息的端口;D 表示的是万一集群中的 Leader 服务器挂了,需要一个端口来重新进行选举,选出一个新的 Leader,而这个端口就是用来执行选举时服务器相互通信的端口。如果是伪集群的配置方式,由于 B 都是一样,所以不同的 Zookeeper 实例通信端口号不能一样,所以要给它们分配不同的端口号。

除了修改 zoo.cfg 配置文件,集群模式下还要配置一个文件 myid,这个文件在 dataDir 目录下,这个文件里面就有一个数据就是 A 的值,Zookeeper 启动时会读取这个文件,拿到里面的数据与 zoo.cfg 里面的配置信息比较从而判断到底是那个 server。


观点仅代表自己,期待你的留言。

https://www.ibm.com/developerworks/cn/opensource/os-cn-zookeeper/

Redis集群备注

介绍

  1. Redis集群是一个可以在多个Redis节点之间进行数据共享的设施(installation)。
  2. Redis集群不支持那些需要同时处理多个键的Redis命令,因为执行这些命令需要在多个Redis节点之间移动数据,并且在高负载的情况下,这些命令将降低Redis集群的性能,并导致不可预测的行为。
  3. Redis集群通过分区(partition)来提供一定程度的可用性(availability):即使集群中有一部份分节点失效或者无法进行通讯,集群也可以继续处理命令请求。

Redis集群提供了以下两个好处:
1、将数据自动切分(split)到多个节点的能力。
2、当集群中的一部份节点失效或者无法进行通讯时,仍然可以继续处理命令请求的能力。

数据节点分配

Redis 集群没有并使用传统的一致性哈希来分配数据,而是采用另外一种叫做哈希槽 (hash slot)的方式来分配的。redis cluster 默认分配了 16384 个slot,当我们set一个key 时,会用CRC16算法来取模得到所属的slot,然后将这个key 分到哈希槽区间的节点上,具体算法就是:CRC16(key) % 16384。集群中的每个节点负责处理一部分哈希槽。

这种将哈希槽分布到不同节点的做法使得用户可以很容易地向集群中添加或者删除节点。
当添加集群节点时,需要将已有节点的哈希槽移动到新的节点上进行处理。
当删除集群节点时,需要先将节点上已分配的哈希槽移动到其它的节点上再进行删除。

客户端存储分配

当客户端向集群节点中任一节点发出存储或读取请求时,redis节点先根据KeyHash出来的值判断是否属于当前集群节点能进行处理,如果不能处理则会将能完成这一请求的Redis节点信息返回给客户端。客户端将再次向能处理请求的Redis节点发出请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
$ redis-cli -c -p 7000
redis 127.0.0.1:7000> set foo bar
-> Redirected to slot [12182] located at 127.0.0.1:7002
OK
redis 127.0.0.1:7002> set hello world
-> Redirected to slot [866] located at 127.0.0.1:7000
OK
redis 127.0.0.1:7000> get foo
-> Redirected to slot [12182] located at 127.0.0.1:7002
"bar"
redis 127.0.0.1:7000> get hello
-> Redirected to slot [866] located at 127.0.0.1:7000
"world"

集群节点故障

采用数据分片的处理集群数据分配就存在无法避免的问题,当其中一个集群节点挂掉时,分配到此节点上哈希槽的数据将无法处理。
为了使得集群在一部分节点下线或者无法与集群的大多数(majority)节点进行通讯的情况下, 仍然可以正常运作,
Redis 集群对每个集群节点都提供了了主从复制功能: 集群中的每个节点都有 1 个至 N 个复制品(replica), 其中一个复制品为主节点(master), 而其余的 N-1 个复制品为从节点(slave)。


观点仅代表自己,期待你的留言。