0%

问题一:刷新依赖,gradle解析出一个根本不存在的版本号

例如:

1
testRuntime 'org.junit.jupiter:junit-jupiter-api:5.2.0'

解析版本号明显不对,通过gradle下载文件可以发现。

在已知仓库列表中,下载不到依赖对应的metadata文件、pom文件

so.

gradle给出个恶作剧般的提示=。=

问题二:刷新依赖时,偶尔有的包会重新解析

maven有本地仓库

gradle有本地缓存

既然都从本地拿了,为什么有时候会重新下载某些包❓

Gradle 对于动态版本和变化模块的缓存时间默认是 24 小时。

  • 动态版本:3.+ 这种就是动态版本,latest.integration,它也是动态版本
  • 变化模块:0.2-SNAPSHOT 这种后面带 SNAPSHOT 的

修改缓存周期

  • 执行型

  • ```shell
    gradle build –refresh-dependencies

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

    * 配置型

    * ```groovy
    configurations.all {
    // 动态版本
    resolutionStrategy.cacheDynamicVersionsFor 10, 'minutes'
    // 变化模块
    resolutionStrategy.cacheChangingModulesFor 10, 'minutes'
    }

1. 日用而不知

来看一个生活中的例子。比如说,有五把尺子:

用它们来分别测量一线段的长度,得到的数值分别为(颜色指不同的尺子):

之所以出现不同的值可能因为:

  • 不同厂家的尺子的生产精度不同
  • 尺子材质不同,热胀冷缩不一样
  • 测量的时候心情起伏不定
  • ……

总之就是有误差,这种情况下,一般取平均值来作为线段的长度:

日常中就是这么使用的。可是作为很事’er的数学爱好者,自然要想下:

  • 这样做有道理吗?
  • 用调和平均数行不行?
  • 用中位数行不行?
  • 用几何平均数行不行?

2. 最小二乘法

换一种思路来思考刚才的问题。

首先,把测试得到的值画在笛卡尔坐标系中,分别记作$y_i$:

其次,把要猜测的线段长度的真实值用平行于横轴的直线来表示(因为是猜测的,所以用虚线来画),记作$y$:

每个点都向$y$做垂线,垂线的长度就是$|y - y_i|$, 也可以理解为测量值和真实值之间的误差:

因为误差是长度,还要取绝对值,计算起来麻烦,就干脆用平方来代表误差:

​ $\left| y - y _ { i } \right| \rightarrow \left( y - y _ { i } \right) ^ { 2 }$

总的误差的平方就是:

​ $\epsilon = \sum \left( y - y _ { i } \right) ^ { 2 }$

因为$y$是猜测的,所以可以不断变换:

自然,总的误差$\epsilon$也是在不断变化的。

法国数学家,阿德里安-馬里·勒讓德(1752-1833,这个头像有点抽象)提出让总的误差的平方最小的$y$就是真值,这是基于,如果误差是随机的,应该围绕真值上下波动(关于这点可以看下“如何理解无偏估计?”)。

这就是最小二乘法,即:

这个猜想也蛮符合直觉的,来算一下。

这是一个二次函数,对其求导,导数为0的时候取得最小值:

进而:

正好是算术平均数。

原来算术平均数可以让误差最小啊,这下看来选用它显得讲道理了。

以下这种方法:

就是最小二乘法,所谓“二乘”就是平方的意思,台湾直接翻译为最小平方法。

3. 推广

算术平均数只是最小二乘法的特例,适用范围比较狭窄。而最小二乘法用途就广泛。

比如温度与冰淇淋的销量:

看上去像是某种线性关系:

可以假设这种线性关系为:

通过最小二乘法的思想:

上图的$i , x , y$分别为:

总误差的平方为:

不同的$a,b$会导致不同的$\epsilon$,根据多元微积分的知识,当:

这个时候$\epsilon$取最小值。

对于$a, b$而言,上述方程组为线性方程组,用之前的数据解出来:

也就是这根直线:

其实,还可以假设:

在这个假设下,可以根据最小二乘法,算出$a,b,c​$,得到下面这根红色的二次曲线:

同一组数据,选择不同的$f(x)$,通过最小二乘法可以得到不一样的拟合曲线(出处):

不同的数据,更可以选择不同的$f(x)$,通过最小二乘法可以得到不一样的拟合曲线:

$f(x)$也不能选择任意的函数,还是有一些讲究的,这里就不介绍了。

4. 最小二乘法与正态分布

我们对勒让德的猜测,即最小二乘法,仍然抱有怀疑,万一这个猜测是错误的怎么办?

数学王子高斯(1777-1855)也像我们一样心存怀疑。

高斯换了一个思考框架,通过概率统计那一套来思考。

让我们回到最初测量线段长度的问题。高斯想,通过测量得到了这些值:

每次的测量值$x_i$都和线段长度的真值$x$之间存在一个误差:

这些误差最终会形成一个概率分布,只是现在不知道误差的概率分布是什么。假设概率密度函数为:

再假设一个联合概率密度函数,这样方便把所有的测量数据利用起来:

讲到这里,有些同学可能已经看出来了上面似然函数了(关于似然函数以及马上要讲到的极大似然估计,可以参考“如何理解极大似然估计法?”)。

因为$L(x)$是关于$x$的函数,并且也是一个概率密度函数(下面分布图形是随便画的):

根据极大似然估计的思想,概率最大的最应该出现(既然都出现了,而我又不是“天选之才”,那么自然不会是发生了小概率事件),也就是应该取到下面这点:

当下面这个式子成立时,取得最大值:

然后高斯想,最小二乘法给出的答案是:

如果最小二乘法是对的,那么$x = \overline { x }$时应该取得最大值,即:

好,现在可以来解这个微分方程了。最终得到:

这是什么?这就是正态分布啊。

并且这还是一个充要条件:

也就是说,如果误差的分布是正态分布,那么最小二乘法得到的就是最有可能的值。

那么误差的分布是正态分布吗?

我们相信,误差是由于随机的、无数的、独立的、多个因素造成的,比如之前提到的:

  • 不同厂家的尺子的生产精度不同
  • 尺子材质不同,热胀冷缩不一样
  • 测量的时候心情起伏不定
  • ……

那么根据中心极限定理(参考“为什么正态分布如此常见?”),误差的分布就应该是正态分布。

因为高斯的努力,才真正奠定了最小二乘法的重要地位。

常见场景:

  • 计费公式
  • 规则决策

需求

1. 计费公式

  • 精确度要求高
  • 支持数值运算

2. 规则决策

  • 支持逻辑运算
  • 支持数值运算

实现

1. evalEx

成熟的表达式计算器,通过解析操作符、数值、变量 计算

1
2
3
4
5
6
7
</dependencies>
<dependency>
<groupId>com.udojava</groupId>
<artifactId>EvalEx</artifactId>
<version>2.1</version>
</dependency>
</dependencies>
1
2
3
4
5
6
// 创建表达式
Expression expression = new Expression("amount * 0.1");
// 置入变量
expression.setVariable("amount", request.getAmount());
// 计算
BigDecimal res = expression.eval();

2. groovy

groovy可编译成class文件,与java平滑整合

jsr-223脚本接口调用简单

1
2
3
4
5
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<scope>test</scope>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
// 创建脚本引擎
ScriptEngineManager factory = new ScriptEngineManager();
ScriptEngine engine = factory.getEngineByName("groovy");

// 简单数值运算
Integer sum = (Integer) engine.eval("(1..10).sum()");
assertEquals(new Integer(55), sum);

// 带变量的运算(脚本只需编译一次)
engine.eval("def func01(amount){ 0.01 * amount + 1}");
Invocable inv = (Invocable) engine;
Object res = ((Invocable) engine).invokeFunction("func01", new BigDecimal(5.0));

ps:

不足

  • groovy脚本中默认的小数为bigdecimal;相反,非小数时,精度问题突出
  • 虽然可以人为将表达式,写成小数的。但略复杂
  • 编译执行,缓存脚本费点神

优势

  • 编译执行,效率比工具一高
  • 支持函数更丰富(前提,熟悉groovy)

前言

1
2
3
4
// 注入所有HttpMessageConverters

@Autowired
private ObjectFactory<HttpMessageConverters> messageConverters;
  • 为什么需要objectFactory包装?

接口定义

1
2
3
4
5
6
7
8
9
10
11
public interface ObjectFactory<T> {

/**
* Return an instance (possibly shared or independent)
* of the object managed by this factory.
* @return an instance of the bean (should never be {@code null})
* @throws BeansException in case of creation errors
*/
T getObject() throws BeansException;

}

常见实现类

  1. RequestObjectFactory
  2. ResponseObjectFactory
  3. SessionObjectFactory
  4. WebRequestObjectFactory
  5. TargetBeanObjectFactory
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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
public abstract class WebApplicationContextUtils {
/**
* Factory that exposes the current request object on demand.
*/
@SuppressWarnings("serial")
private static class RequestObjectFactory implements ObjectFactory<ServletRequest>, Serializable {

@Override
public ServletRequest getObject() {
return currentRequestAttributes().getRequest();
}

@Override
public String toString() {
return "Current HttpServletRequest";
}
}


/**
* Factory that exposes the current response object on demand.
*/
@SuppressWarnings("serial")
private static class ResponseObjectFactory implements ObjectFactory<ServletResponse>, Serializable {

@Override
public ServletResponse getObject() {
ServletResponse response = currentRequestAttributes().getResponse();
if (response == null) {
throw new IllegalStateException("Current servlet response not available - " +
"consider using RequestContextFilter instead of RequestContextListener");
}
return response;
}

@Override
public String toString() {
return "Current HttpServletResponse";
}
}


/**
* Factory that exposes the current session object on demand.
*/
@SuppressWarnings("serial")
private static class SessionObjectFactory implements ObjectFactory<HttpSession>, Serializable {

@Override
public HttpSession getObject() {
return currentRequestAttributes().getRequest().getSession();
}

@Override
public String toString() {
return "Current HttpSession";
}
}


/**
* Factory that exposes the current WebRequest object on demand.
*/
@SuppressWarnings("serial")
private static class WebRequestObjectFactory implements ObjectFactory<WebRequest>, Serializable {

@Override
public WebRequest getObject() {
ServletRequestAttributes requestAttr = currentRequestAttributes();
return new ServletWebRequest(requestAttr.getRequest(), requestAttr.getResponse());
}

@Override
public String toString() {
return "Current ServletWebRequest";
}
}

}

测试用例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 控制层基类
*/
public abstract class BaseController {

@Autowired
protected HttpServletRequest request;

@Autowired
protected HttpServletResponse response;

@Autowired
protected HttpSession session;
}

// 子类
@Controller
public class SystemController extends BaseController {
........
............
}

咋一看,是不是觉得存在线程安全问题? 在SpringMvc中Controller不是单例的吗?那request,response和session对象岂不是所有线程共享的,后面进来的请求岂不是会覆盖前面请求的这些对象。但事实上,官方已经给出证明example,这种写法是正确的,不会存在线程安全问题,下面来分析一下,Spring是如何实现的。

ps: ResponseObjectFactory 是从Spring 4版本以上才开始出现的。

实现原理

XmlWebApplicationContext是Spring提供的容器的一种实现,多运用于Web项目中。继承于AbstractRefreshableWebApplicationContext,而AbstractRefreshableWebApplicationContext重写了AbstractApplicationContext中的postProcessBeanFactory方法,向beanFactory容器中添加依赖解析映射关系。

AbstractApplicationContext:

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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
public abstract class AbstractApplicationContext extends DefaultResourceLoader
implements ConfigurableApplicationContext, DisposableBean {
@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// Prepare this context for refreshing.
prepareRefresh();

// Tell the subclass to refresh the internal bean factory.
// 加载Bean信息,并添加到容器中
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);

try {
// Allows post-processing of the bean factory in context subclasses.
// 调用子类重写的方法
postProcessBeanFactory(beanFactory);

// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);

// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);

// Initialize message source for this context.
initMessageSource();

// Initialize event multicaster for this context.
initApplicationEventMulticaster();

// Initialize other special beans in specific context subclasses.
onRefresh();

// Check for listener beans and register them.
registerListeners();

// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);

// Last step: publish corresponding event.
finishRefresh();
}
catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}

// Destroy already created singletons to avoid dangling resources.
destroyBeans();

// Reset 'active' flag.
cancelRefresh(ex);

// Propagate exception to caller.
throw ex;
}
finally {
// Reset common introspection caches in Spring's core, since we
// might not ever need metadata for singleton beans anymore...
resetCommonCaches();
}
}
}

/**
* Modify the application context's internal bean factory after its standard
* initialization. All bean definitions will have been loaded, but no beans
* will have been instantiated yet. This allows for registering special
* BeanPostProcessors etc in certain ApplicationContext implementations.
* @param beanFactory the bean factory used by the application context
*/
protected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
}

}

AbstractRefreshableWebApplicationContext是AbstractApplicationContext的子类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public abstract class AbstractRefreshableWebApplicationContext extends AbstractRefreshableConfigApplicationContext
implements ConfigurableWebApplicationContext, ThemeSource {

@Override
protected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
beanFactory.addBeanPostProcessor(new ServletContextAwareProcessor(this.servletContext, this.servletConfig));
beanFactory.ignoreDependencyInterface(ServletContextAware.class);
beanFactory.ignoreDependencyInterface(ServletConfigAware.class);

// 关键步骤:添加解析映射关系
WebApplicationContextUtils.registerWebApplicationScopes(beanFactory, this.servletContext);
WebApplicationContextUtils.registerEnvironmentBeans(beanFactory, this.servletContext, this.servletConfig);
}

}

WebApplicationContextUtils:

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
public abstract class WebApplicationContextUtils {

/**
* Register web-specific scopes ("request", "session", "globalSession", "application")
* with the given BeanFactory, as used by the WebApplicationContext.
* @param beanFactory the BeanFactory to configure
* @param sc the ServletContext that we're running within
*/
public static void registerWebApplicationScopes(ConfigurableListableBeanFactory beanFactory, ServletContext sc) {
beanFactory.registerScope(WebApplicationContext.SCOPE_REQUEST, new RequestScope());
beanFactory.registerScope(WebApplicationContext.SCOPE_SESSION, new SessionScope(false));
beanFactory.registerScope(WebApplicationContext.SCOPE_GLOBAL_SESSION, new SessionScope(true));
if (sc != null) {
ServletContextScope appScope = new ServletContextScope(sc);
beanFactory.registerScope(WebApplicationContext.SCOPE_APPLICATION, appScope);
// Register as ServletContext attribute, for ContextCleanupListener to detect it.
sc.setAttribute(ServletContextScope.class.getName(), appScope);
}

// 向容器中添加依赖解析映射关系
beanFactory.registerResolvableDependency(ServletRequest.class, new RequestObjectFactory());
beanFactory.registerResolvableDependency(ServletResponse.class, new ResponseObjectFactory());
beanFactory.registerResolvableDependency(HttpSession.class, new SessionObjectFactory());
beanFactory.registerResolvableDependency(WebRequest.class, new WebRequestObjectFactory());
if (jsfPresent) {
FacesDependencyRegistrar.registerFacesDependencies(beanFactory);
}
}

}

当Spring容器启动的时候,会调用refresh方法,从而触发postProcessBeanFactory方法的执行。向beanFactory中添加依赖解析映射关系,当调用beanFactory.getBean方法获取实例时,注入依赖Bean则会使用到该依赖解析映射关系。

DefaultListableBeanFactory:

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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFactory
implements ConfigurableListableBeanFactory, BeanDefinitionRegistry, Serializable {

/** Map from dependency type to corresponding autowired value */
private final Map<Class<?>, Object> resolvableDependencies = new ConcurrentHashMap<Class<?>, Object>(16);

@Override
public void registerResolvableDependency(Class<?> dependencyType, Object autowiredValue) {
Assert.notNull(dependencyType, "Dependency type must not be null");
if (autowiredValue != null) {
if (!(autowiredValue instanceof ObjectFactory || dependencyType.isInstance(autowiredValue))) {
throw new IllegalArgumentException("Value [" + autowiredValue +
"] does not implement specified dependency type [" + dependencyType.getName() + "]");
}
this.resolvableDependencies.put(dependencyType, autowiredValue);
}
}

/**
* Find bean instances that match the required type.
* Called during autowiring for the specified bean.
* @param beanName the name of the bean that is about to be wired
* @param requiredType the actual type of bean to look for
* (may be an array component type or collection element type)
* @param descriptor the descriptor of the dependency to resolve
* @return a Map of candidate names and candidate instances that match
* the required type (never {@code null})
* @throws BeansException in case of errors
* @see #autowireByType
* @see #autowireConstructor
*/
protected Map<String, Object> findAutowireCandidates(
String beanName, Class<?> requiredType, DependencyDescriptor descriptor) {

String[] candidateNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
this, requiredType, true, descriptor.isEager());
Map<String, Object> result = new LinkedHashMap<String, Object>(candidateNames.length);
for (Class<?> autowiringType : this.resolvableDependencies.keySet()) {
// 判断当前依赖Bean类型requiredType是否是autowiringType及其子类
if (autowiringType.isAssignableFrom(requiredType)) {
// 如果是则使用映射关系值,如:RequestObjectFactory等
Object autowiringValue = this.resolvableDependencies.get(autowiringType);
// 关键步骤:生成JDK动态代理
autowiringValue = AutowireUtils.resolveAutowiringValue(autowiringValue, requiredType);
if (requiredType.isInstance(autowiringValue)) {
result.put(ObjectUtils.identityToString(autowiringValue), autowiringValue);
break;
}
}
}
for (String candidate : candidateNames) {
if (!isSelfReference(beanName, candidate) && isAutowireCandidate(candidate, descriptor)) {
addCandidateEntry(result, candidate, descriptor, requiredType);
}
}
if (result.isEmpty() && !indicatesMultipleBeans(requiredType)) {
// Consider fallback matches if the first pass failed to find anything...
DependencyDescriptor fallbackDescriptor = descriptor.forFallbackMatch();
for (String candidate : candidateNames) {
if (!isSelfReference(beanName, candidate) && isAutowireCandidate(candidate, fallbackDescriptor)) {
addCandidateEntry(result, candidate, descriptor, requiredType);
}
}
if (result.isEmpty()) {
// Consider self references as a final pass...
// but in the case of a dependency collection, not the very same bean itself.
for (String candidate : candidateNames) {
if (isSelfReference(beanName, candidate) &&
(!(descriptor instanceof MultiElementDescriptor) || !beanName.equals(candidate)) &&
isAutowireCandidate(candidate, fallbackDescriptor)) {
addCandidateEntry(result, candidate, descriptor, requiredType);
}
}
}
}
return result;
}

}

AutowireUtils:

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
55
56
57
58
59
60
61
62
63
abstract class AutowireUtils {

/**
* Resolve the given autowiring value against the given required type,
* e.g. an {@link ObjectFactory} value to its actual object result.
* @param autowiringValue the value to resolve
* @param requiredType the type to assign the result to
* @return the resolved value
*/
public static Object resolveAutowiringValue(Object autowiringValue, Class<?> requiredType) {
if (autowiringValue instanceof ObjectFactory && !requiredType.isInstance(autowiringValue)) {
ObjectFactory<?> factory = (ObjectFactory<?>) autowiringValue;
if (autowiringValue instanceof Serializable && requiredType.isInterface()) {
// 如果是接口则生成基于JDK的动态代理
autowiringValue = Proxy.newProxyInstance(requiredType.getClassLoader(),
new Class<?>[] {requiredType}, new ObjectFactoryDelegatingInvocationHandler(factory));
}
else {
// 不是接口则返回factory.getObject()对象
return factory.getObject();
}
}
// 返回原始值
return autowiringValue;
}

/**
* Reflective InvocationHandler for lazy access to the current target object.
*/
@SuppressWarnings("serial")
private static class ObjectFactoryDelegatingInvocationHandler implements InvocationHandler, Serializable {

private final ObjectFactory<?> objectFactory;

public ObjectFactoryDelegatingInvocationHandler(ObjectFactory<?> objectFactory) {
this.objectFactory = objectFactory;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
if (methodName.equals("equals")) {
// Only consider equal when proxies are identical.
return (proxy == args[0]);
}
else if (methodName.equals("hashCode")) {
// Use hashCode of proxy.
return System.identityHashCode(proxy);
}
else if (methodName.equals("toString")) {
return this.objectFactory.toString();
}
try {
// 通过反射调用,如RequestObjectFactory等中的方法
return method.invoke(this.objectFactory.getObject(), args);
}
catch (InvocationTargetException ex) {
throw ex.getTargetException();
}
}
}

}

我们在回到WebApplicationContextUtils类中的RequestObjectFactory,看下其具体的实现。

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
public abstract class WebApplicationContextUtils {

/**
* Factory that exposes the current request object on demand.
*/
@SuppressWarnings("serial")
private static class RequestObjectFactory implements ObjectFactory<ServletRequest>, Serializable {

@Override
public ServletRequest getObject() {
return currentRequestAttributes().getRequest();
}

@Override
public String toString() {
return "Current HttpServletRequest";
}
}

/**
* RequestContextHolder 从当前请求上下文中获取真实的Request,Response,Session等对象
* Return the current RequestAttributes instance as ServletRequestAttributes.
* @see RequestContextHolder#currentRequestAttributes()
*/
private static ServletRequestAttributes currentRequestAttributes() {
RequestAttributes requestAttr = RequestContextHolder.currentRequestAttributes();
if (!(requestAttr instanceof ServletRequestAttributes)) {
throw new IllegalStateException("Current request is not a servlet request");
}
return (ServletRequestAttributes) requestAttr;
}

}

请求线程上下文管理器RequestContextHolder:

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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
public abstract class RequestContextHolder  {
private static final ThreadLocal<RequestAttributes> requestAttributesHolder =
new NamedThreadLocal<RequestAttributes>("Request attributes");

private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder =
new NamedInheritableThreadLocal<RequestAttributes>("Request context");

/**
* Bind the given RequestAttributes to the current thread.
* @param attributes the RequestAttributes to expose,
* or {@code null} to reset the thread-bound context
* @param inheritable whether to expose the RequestAttributes as inheritable
* for child threads (using an {@link InheritableThreadLocal})
*/
public static void setRequestAttributes(RequestAttributes attributes, boolean inheritable) {
if (attributes == null) {
resetRequestAttributes();
}
else {
if (inheritable) {
inheritableRequestAttributesHolder.set(attributes);
requestAttributesHolder.remove();
}
else {
requestAttributesHolder.set(attributes);
inheritableRequestAttributesHolder.remove();
}
}
}

/**
* Return the RequestAttributes currently bound to the thread.
* @return the RequestAttributes currently bound to the thread,
* or {@code null} if none bound
*/
public static RequestAttributes getRequestAttributes() {
RequestAttributes attributes = requestAttributesHolder.get();
if (attributes == null) {
attributes = inheritableRequestAttributesHolder.get();
}
return attributes;
}

/**
* Return the RequestAttributes currently bound to the thread.
* <p>Exposes the previously bound RequestAttributes instance, if any.
* Falls back to the current JSF FacesContext, if any.
* @return the RequestAttributes currently bound to the thread
* @throws IllegalStateException if no RequestAttributes object
* is bound to the current thread
* @see #setRequestAttributes
* @see ServletRequestAttributes
* @see FacesRequestAttributes
* @see javax.faces.context.FacesContext#getCurrentInstance()
*/
public static RequestAttributes currentRequestAttributes() throws IllegalStateException {
RequestAttributes attributes = getRequestAttributes();
if (attributes == null) {
if (jsfPresent) {
attributes = FacesRequestAttributesFactory.getFacesRequestAttributes();
}
if (attributes == null) {
throw new IllegalStateException("No thread-bound request found: " +
"Are you referring to request attributes outside of an actual web request, " +
"or processing a request outside of the originally receiving thread? " +
"If you are actually operating within a web request and still receive this message, " +
"your code is probably running outside of DispatcherServlet/DispatcherPortlet: " +
"In this case, use RequestContextListener or RequestContextFilter to expose the current request.");
}
}
return attributes;
}

}

当请求进来时,设置当前请求的上下文信息,RequestContextFilter类。RequestContextFilter:

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
public class RequestContextFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {

ServletRequestAttributes attributes = new ServletRequestAttributes(request, response);
initContextHolders(request, attributes);

try {
filterChain.doFilter(request, response);
}
finally {
resetContextHolders();
if (logger.isDebugEnabled()) {
logger.debug("Cleared thread-bound request context: " + request);
}
attributes.requestCompleted();
}
}

// 设置请求上下文信息
private void initContextHolders(HttpServletRequest request, ServletRequestAttributes requestAttributes) {
LocaleContextHolder.setLocale(request.getLocale(), this.threadContextInheritable);
RequestContextHolder.setRequestAttributes(requestAttributes, this.threadContextInheritable);
if (logger.isDebugEnabled()) {
logger.debug("Bound request context to thread: " + request);
}
}

private void resetContextHolders() {
LocaleContextHolder.resetLocaleContext();
RequestContextHolder.resetRequestAttributes();
}

}

至此,怎个流程算是串起来了,之所以不会存在线程安全问题,那是因为注入的是实现了HttpServletRequest, HttpServletResponse接口的动态代理对象,然后在这个动态代理对象中执行invoke时,获取当前线程上下文所关联的实际request,response等对象,再反射执行其中的方法。整体设计非常巧妙,设计思想值得学习和借鉴。

后记

objectFactory有实现类:org.springframework.beans.factory.support.DefaultListableBeanFactory.DependencyObjectProvider

通过它,可以实现bean的懒加载。即,第一次调用getObject时,解析所有spring管理的泛型bean,返回。

  • @BootstrapWith
    • SpringTestContextFramework启动类
  • @ContextConfiguration
    • 默认当前应用环境
    • SpringTestContextFramework启动配置,要加载的类等
    • 控制加载项的多少,影响环境准备时间
  • @ActiveProfiles
    • 启用的环境
  • @TestPropertySource
    • 属性文件指定
  • @DirtiesContext
    • applicationContext是否清除
  • @WebAppConfiguration
    • 申明这是一个web项目,指定web资源路径
  • @TestExecutionListeners
    • 监听测试类、方法
  • @Transactional
    • 事务申明
  • @BeforeTransaction
  • @AfterTransaction
  • @Commit
    • 测试方法结束提交事务
  • @Rollback
    • 测试方法结束回滚事物
  • @Sql
    • 方法、类执行前 初始化数据等
  • @SqlConfig
    • sql指定的文件配置项
  • @SqlGroup
  • @Repeat (only supported on JUnit 4)
    • 方法重复执行几次
  • @Timed (only supported on JUnit 4)
    • 方法执行最大时间,超出失败
  • @IfProfileValue (only supported on JUnit 4)
    • 根据属性值,决定是否执行该方法
  • @ProfileValueSourceConfiguration (only supported on JUnit 4)
    • 重写配置项

数坑:

  1. itext本身不支持中文
    1. 字体文件格式的不同ttc\ttf,造成加载方式差异
  2. itext7之前,官方jar不支持中文自动换行
    1. 排版调整,极其艰难
  3. itext不支持字体加粗
    1. 解决方式:导入加粗、非加粗两种字体
    2. 字体选择
  4. 严格要求html标签开闭
  5. itext7默认的透明度,拿粉色遮罩
  6. itext下划线问题

从html转pdf,难点主要在两种文件格式语法不一样。需要把html语法转换成pdf,那么html对于排版的规范、优化也必然要被支持。

由此造成工作的繁琐复杂。

1. 引入jar

1
2
3
# 包含itext7大多基本包

compile group: 'com.itextpdf', name: 'html2pdf', version: '2.1.1'

2. 中文字体支持

1
2
3
4
5
6
7
8
9
10
11
12
ConverterProperties properties = new ConverterProperties();
FontProvider font = new FontProvider();

// fontPath = "xxxxxxx/xxx/xxxx.ttf"
// fontPath = "xxxx/xxx/xxx.tcc,1" tcc是个字体合集,取其索引为1的
PdfFont pdfFont = PdfFontFactory.createTtcFont(fontPath, 1, PdfEncodings.IDENTITY_H,false, true);

font.addFont(pdfFont.getFontProgram());
// font.addSystemFonts();
properties.setFontProvider(font);

Optional<FontInfo> fontinfo = font.getFontSet().getFonts().stream().findFirst();

3. html2pdf

1
HtmlConverter.convertToPdf(htmlFile,outputPdf,properties);

4. 水印

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
PdfDocument pdfDocument = new PdfDocument(new PdfReader(srcFile), new PdfWriter(targetFile));
doc = new Document(pdfDocument);

// 添加图层的属性配置
PdfExtGState gs1 = new PdfExtGState();
// 透明度设置
gs1.setFillOpacity(1f);

for (int i = 1; i <= pdfDocument.getNumberOfPages(); i++) {
// 条形码水印到pdf
ImageData imageData = ImageDataFactory.create(barCodePath);// 插入水印

// 相对定位使用
Rectangle pageSize = pdfDocument.getPage(i).getPageSize();
float x, y;
x = signConfig.getSignX() > 0 ? signConfig.getSignX().floatValue() :
pageSize.getWidth() + signConfig.getSignX().floatValue() - signConfig.getSignWidth().floatValue();
y = signConfig.getSignY() > 0 ? signConfig.getSignY().floatValue() :
pageSize.getHeight() + signConfig.getSignY().floatValue() - signConfig.getSignHeight().floatValue();

PdfCanvas pdfCanvas = new PdfCanvas(pdfDocument.getPage(i));
pdfCanvas.saveState();
pdfCanvas.setExtGState(gs1);
pdfCanvas.addImage(imageData, x, y, signConfig.getSignWidth().floatValue(), false);
pdfCanvas.restoreState();
}

doc.close();

5. 下划线

实现1

1
<u></u>
1
text-decoration: underline

效果如下(支持换行):

image-20181225165825747

实现2

1
border-bottom: 1px solid #949494;

效果如下(不支持换行,这里是手动分行):

引言

分布式系统中,常常涉及外部资源调用。出于开发进度,沟通、制度等原因,这些资源存在不可调用的情况。

为此,我们有两种解决方案

  • 自建http server,响应系统请求
    • eoLinker
    • postman
  • 项目内,代理目标资源调用,使不调用或返回固定信息
    • mockito

基础

引入jar包
1
2
3
4
# gradle为例

repositories { jcenter() }
dependencies { testCompile "org.mockito:mockito-core:2.+" }
stub method
1
2
3
4
5
6
7
8
9
10
11
// 定义mock对象
LinkedList mockedList = mock(LinkedList.class);

// mock对象调用指定方法、指定参数时,返回 "first"
when(mockedList.get(0)).thenReturn("first");

// "first"
System.out.println(mockedList.get(0));

// null
System.out.println(mockedList.get(999));
方法调用校验
1
2
3
4
5
6
7
8
// 方法被调用5次,指定参数
verify(mock, times(5)).someMethod("was called five times");

// 方法被调用至少2次,指定参数
verify(mock, atLeast(2)).someMethod("was called at least two times");

// 方法被调用至少一次,且参数只要是string
verify(mock, atLeastOnce()).someMethod(anyString());

与junit/spring整合

基类,测试类共享application-context

1
2
3
4
5
@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoApplicationTests {

}

具体测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
// 从spring容器注入对象(真实)

public class ContractCreateServiceTest extends DemoApplicationTests{

@Autowired
private ContractCreateService contractCreateService;

@Test
public void create() throws Exception {
ContractCreateParam createParam = new ContractCreateParam();
contractCreateService.create(createParam);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// mock一个对象,不做stub,调用其方法默认返回null

public class ContractCreateServiceTest extends DemoApplicationTests{

@Mock
private ContractCreateService contractCreateService;

@Test
public void create() throws Exception {
ContractCreateParam createParam = new ContractCreateParam();
contractCreateService.create(createParam);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// mock一个对象,默认调用真实方法,除非手动when().thenReturn()

public class ContractCreateServiceTest extends DemoApplicationTests{

@Spy
private ContractCreateService contractCreateService;

@Test
public void create() throws Exception {
ContractCreateParam createParam = new ContractCreateParam();
contractCreateService.create(createParam);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// mock对象被注入另一个对象(@Spy注解的也会被注入)

public class ContractCreateServiceTest extends DemoApplicationTests{

@Mock
private ContractCreateManager contractCreateManager;

@InjectMocks
private ContractCreateService contractCreateService;

@Test
public void create() throws Exception {
ContractCreateParam createParam = new ContractCreateParam();
contractCreateService.create(createParam);
}
}

后记

mock帮助你调用特定对象、方法、参数时,返回指定结果。就像给对象加一个环绕切面。

在单元测试中,还需要对返回结果执行校验

请参考

  • junit assert
  • Spock expect

参考文献:

1. 官方文档(带代码)

1.系统架构

该分布式区块链系统包含很多节点,他们互相通信,构成一个整体。

chaincode 是区块链系统运行的程序,它维护世界观状态、账本数据,并且执之行事务;chaincode 是事务的核心元素,因为事物过程就是chaincode 上的一系统操作。

事务需要被背书,并且只有被背书的事务才能提交,进而影响世界观。

程序中有一个或多个特殊的chaincode ,他们被用来管理方法合参数。这些链码统称为系统链码。

1.1事物-transaction

可能的两种类型事务:

  • ”部署事务“:创建新的链码,并且把一段程序作为参数。当一个部署事务之行成功的时候,链码就被安装到区块链上了
  • ”调用事务“:在之前部署链码的环境中,执行一个操作。一个事务调用对应一段链码合它的一个方法。当调用成功的时侯,链码会执行指定方法,修改对应的世界观,然后返回一个输出结果。

正如后面描述的,部署事务是调用事务的一个特殊场景。部署事务创建新的链码,等价于在系统链码上调用一个事务。

1.2区块链数据结构

1.2.1 世界观-state

最新的区块链世界观,被建模成一个版本话的键值对数据库。其中key是名字,value是任意的字符。由chaincode 完成put、get操作,持久化保存世界观并记录操作日志。

注意:版本话的键值对数据库适配任何状态模型,不管是kvs、rdms亦或者其他解决方案。

1.2.2账本-ledger

账本记录可验证的世界观变更日志。

账本是由order service 构造的一个有序哈希链表块。其强制包含所有区块,且每个区块包含一组全局有序的事务。

账本也是一个备选的orderer 。区别于orderLedgerpeerLedger 本地有一个标记,用于区分合法、非法事务。

1.3 节点-node

三种节点:

  1. client
    1. 提交事务到背书人
    2. 广播事务到orderer
  2. peer
    1. 特定的是背书人角色
    2. 提交事务
    3. 管理世界观
    4. 保存一份完整的账本
  3. orderer
    1. 通信,分发消息
    2. 原子操作,广播

1.3.1 client

client 响应终端用户行为。它必须连一个peer ,借此与区块链通信。

client 可以连接任意可选的peer节点,创建、调用事务。

1.3.2 peer

除了普通职责外,peer是一个可选的背书人节点。

每个chaincode 可以指定一个背书策略(包含一系列背书节点)。背书策略定义了合法交易背书的充分必要条件。在部署事务中,背书策略是系统chaincode

1.3.3 orderer

fabric通讯中,订单分发的职责人。可以有多种实现,从集中服务到分布式协议等等。

  • 分区:支持多channel,保证不通channel消息无干扰
  • API:peer连接到order service的channel,需要实现两个基本方法
    • broadcast(blob) 客户端调用peer,广播消息到orderer
    • deliver(seqno, prevhash, blob) orderer调用peer,创建新的区块

ps:

  • 一致性职责
    • 每个广播消息,只被分发一次
    • 组成有序消息链
  • 分发职责
    • 保证每一个网络连接完好的peer,接收到事务提交消息

2.基本的事务背书流程

  1. 请求事务背书
    1. 模拟事务执行,签名
  2. 收集背书信息,匹配背书策略
    1. 匹配不上,稍后重试等等
  3. 广播事务到order service
  4. 分发事务到peer
    1. 确认事务正好执行完seqno - 1
    2. 背书策略,验证背书签名信息
    3. 匹配版本号,防并发
    4. 提交事务,写入世界观

简介

  • 数据库分布式事务实现
    • jtom,
    • atomikos
  • 实现参与主体
    • 用户API
    • 事务管理器
    • 资源管理器
  • XA协议
    • 事务管理器,操作资源管理器的API
  • 应用场景
    • 单体应用多数据库

XA协议时序图

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
participant 开发人员API as CUS
participant 事务管理器 as TM
participant 资源管理器(数据库A) as DBA
participant 资源管理器(数据库B) as DBB

#TM->DBA: xa_open:创建连接
#TM->DBB: xa_open:创建连接
#Note left of CUS: 初始化完成
CUS->TM: 发起分布式事务:新的XID
CUS->TM: 操作数据库A
TM->DBA: 开启事务
DBA-->TM: 绑定事务A到XID
TM->DBA: 预提交
DBA-->TM: 成功
TM-->CUS: 成功

CUS->TM: 操作数据库B
TM->DBB: 开启事务
DBB-->TM: 绑定事务B到XID
TM->DBB: 预提交
DBB-->TM: 成功
TM-->CUS: 成功

CUS->TM: 提交
TM->DBA: 提交
DBA-->TM: 成功
TM->DBB: 提交
DBB-->TM: 成功
TM-->CUS: 成功
参考
  1. XA协议解释

1、场景说明

背景:

​ 分布式环境下,为保证事务一致性,通常采用两种算法:

  1. 二阶段提交

    • 实现方式
      • 锁定资源 - (确认占用 or 撤销占用)
      • try - (confirm or cancel)
      • 加锁A/预操作A - 加锁B/预操作B(确认A、B均能正确执行) - 提交(确认 or 取消)A/B操作 - 释放锁A/B
    • 存在问题(TCC为例)
      • 不能保证confirm、cancel一定都成功
        • 需要补偿机制
      • try阶段占用资源粒度不能过大,否则影响性能
        • 依业务而定,一般都可以缩小粒度到people 或者 product
        • 上面这条可以无视,我觉着程序员没有傻子
      • try阶段占用资源。如果不能及时confirm or cancel,会影响一致性。
        • 依业务而定,评估业务链条时间消耗,给出超时时间、补偿触发时间
        • 过早触发补偿,导致任务进行中就被撤销,业务成功率低。
        • 过晚触发补偿,导致资源长期占用,数据不一致。
  2. 三阶段提交

    1. 解决的问题

      • 二阶段提交,不能保证原子性
    2. 实现方式

      • 一阶段:

        • 发送消息到参与者
        • 确认是否能执行(一般为资源申请)
          • 是:进入阶段二
          • 否:中断操作
      • 二阶段

        • 发送消息到参与者
        • 预执行,记录undo,redo日志
        • 异常:参与者接不到协调者请求
          • 引发协调者超时
        • 异常-协调者超时:协调者接不到ACK响应,或者未全部接到
          • 发送abort给参与者
          • 中断事务

      • 三阶段

        • 提交事务

          • 参与者收到信号,提交
          • ==参与者未收到信号==,超时自动提交
    3. 存在问题:

      1. 确认提交时,协调者宣布abort,但是参与者未收到

        • 二阶段提交中,导致事务状态暂停———解决——记录协调者对事务的处理状态,做补偿机制
        • 三阶段提交中,导致参与者错误的提交请求———-解决——–同上———-
      2. 协调者故障

        • 协调者集群
        • 频繁失败,检测自身与各参与者网络连通性。从而将问题机器下线

2、TCC实战

  1. 常见流程
    1. 发起事务T,记录GLOBAL ID
    2. 调用服务A try,为服务A创建子事务,加入事务T
    3. 调用服务B try,为服务B创建子事务,加入事务T
    4. 根据服务调用结果,判定事务T,宣布confirm 或者cancel
    5. 服务A、B执行T宣布的结果
  2. 发起者本地事务
    1. 远程事务放在本地事务内,本地事务异常时,同步cancel远程事务
    2. 远程事务放在本地事务外,本地事务也当做远程事务处理
  3. 事务嵌套与事务传播
    1. 开启新事务,隔离
    2. 加入原有事务
  4. 补偿机制
    1. 超时时间设置
    2. 补偿次数
    3. 补偿间隔
    4. 防止并发
  5. 故障恢复
    1. 查找未完结事务
    2. confirm 或者 cancel