Flex Structure
前言
这两天写了一个研究Flex + Java的例子,供大家参考,这个例子主要是出于以下几点考虑的
1. 系统性能和系统可维护性上的平衡(Value Object lazy load)
2. 开发效率和代码可读性上的平衡(Command and CommandManager)
3. 如何让Flex调用服务端的Service(AMF3, Remote Object)
4. 使用Cache Framework提升我们的性能
花絮:其实做项目和生活,管理等等都是一样,做到最好是不太现实的,但要和谐,什么叫和谐?就是在成本,进度,质量等外在压力下把代码写得最好!所以我下面的例子代码也是一样,追求的是一个平衡J
一. 系统性能和系统可维护性上的平衡(Value Object lazy load)
最佳性能时,系统只在网络上传输必要的数据,如显示用户清单时只传输user name和department name。
而结构最优时,传输的却是规范的数据结构。
这个时候矛盾来了
A. 传输规范的数据结构。这时候必然会带上一些冗余数据,如显示用户清单时传输的UserVO,而UserVO里同时也包含了标志这个用户部门的DepartmentVO,这时就会带来不必要的数据传输,如果显示的用户清单有100条,那么这100个UserVO里面的DepartmentVO必然会带来不小的数据冗余。
B. 只在网络上传输必要的数据。这时有两种方法可以做到,设计一个UserListVO,里面包含user name和department name这两样field,然后在Business Logic里组装这个UserListVO。但这种方法显然有个大的缺点,这个VO或对应的业务逻辑代码不可以共用,因为不同的地方会有不同的业务需求,比如有一个模块中会要显示用户的年龄。另一个方法就是,使用规范的数据结构,但只为这些数据结构中必要的栏位设值,如上面所说的,可以只为userVO.departmentVO.name设值,但其它栏位保持null,显然,这个VO的共用性也不好,因为我没法知道这个VO里面的栏位是否已经被设值了。
综上所说,所以我取上面两种方法的一个中间点来解决这个问题(如下图),即使用完整的数据结构来存储数据,但不是必要的数据不会被加载上来,如果要用时,可以通过Lazy Load的方式加载。如UserVO里有DepartmentVO,但在显示清单时不需要user对应的department信息,在编辑时才需要,所以我们可以在popup出用户编辑窗口的时候才在UserVO的getDepartmentVO()方法中加载相应的DepartmentVO。
class diagram for data model
请参见附件中的class diagram for data model
二. 开发效率和代码可读性上的平衡(Command and CommandManager)
往往在开发的时候,标准的结构会多写很多代码,虽然结构很清晰,但老实说,对于我们的项目,好像不需要这样“清晰”,比如Cairngorm中有command, event, controller等等,这确实是一种清晰的结构,但写起来很麻烦,所以我下面设计了一种简化的结构来实现它(如下图)。
class diagram for command
Class Diagram
请参见附件中的class diagram for command
Cache
sequence diagram for command pattern
Sequence Diagram
请参见附件中的sequence diagram for command pattern
关于Command Pattern,请参考以下的链接
http://www.javaworld.com/javaworld/jw-06-2002/jw-0628-designpatterns.html
这里,CommandManager就是那个Invoker。而com.novem.farc.command.UserSaveCommand.datagrid就是那个receiver。
Why not Cairngorm Event or Command?
我们以查找一个user为例,来看看Cairngorm是怎么调用一个Command并返回结果的。
1. 创建一个CairngormEvent,并在这个Event里要有一个userId:Number的field。
2. 创建一个Command,这个Command要实现两个接口,ICommand和IResponder。
3. 创建一个FrontController来建立Event和Command的关连。
然后,在客户端调用的时候,书写如下的代码:
var event: EventFindUser = new EventFindUser ();
event.userId = userVO.id;
CairngormEventDispatcher.getInstance().dispatchEvent( event );
我们现在新的结构是这样实现的:
var command:CommandFindUser = new CommandFindUser();
command.userId = userVO.id;
NovemCommandManager.execute(command);
可以看出来,Cairngorm通过注册Event,并通过Event来传递输入参数,而我们自己的结构是将参数直接传递给Command,所以Cairngorm并没有给我们提供特别的方便,反而增加了不少麻烦的Event,而它提供的这种解耦,也并不实在。
Why not Cairngorm Model Locator?
Cairngorm Model Locator提供的其实是一种静态全局变量。
那么,谁都可以来改变这个Model Locator中的值,这显然是一个很危险的事。
如果大家也和我一样认为Cairngorm Model Locator就是一种静态全局变量的话,我想我在这里不用说得太多,只要去查一下静态全局变量的好处坏处就可以了。
三. 如何让Flex调用服务端的Service(AMF3, Remote Object)
暂且假定,我们的项目使用的Remote Object方式去访问服务端
Why not Cairngorm Delegate?
老规矩,我们先来看看Cairngorm是怎么来调用服务端的
1. 在service.xml里添加配置项
2. 创建Delegate.as,并为RemoteObject添加对应的方法(这里需要为每个服务端对象都创建对应的Delegate和方法,工作量不但不小,而且很烦哦)
再来看看我们的写法吧:
1.在ServiceFactory里添加需要调用的Service和method的名字常量
2.调用方法
ServiceFactory.getService(ServiceFactory.USER_BIZ)
.callService(ServiceFactory.USER_BIZ_Insert, [newVO], this.result);
四. 使用Cache Framework提升我们的性能
有空再做哦……
但主要的思路是使用第三方的Cache工具在业务层做
如何在业务层管理你的Cache
上次初步研究了一下前台与后台的关系,但还遗留了一个Server端的Cache问题。
前言
在看过很多的Cache的文章和讨论后,我是这样使用Cache的
1. 在Session的生命周期内使用Hibernate的First Level Cache来缓存对象(数据访问层,细粒度缓存)
2. 使用EHCache对Value Object在业务层做缓存(粗粒度缓存,写代码实现)
为什么我不想使用Hibernate的二级缓存呢?主要有以下几点思考
为了提高它的性能,我们把Cache和持久层关连起来,值得吗?
有必要所有的地方都做Cache吗?这些性能的提升是客户想要的吗?
哪些地方需要做Cache不是只有业务层才知道吗?
关于Hibernate二级缓存详细的介绍,大家还是看看下面几篇文章吧,讲得很好
分析Hibernate的缓存机制
http://www.enet.com.cn/article/2008/0115/A20080115110243.shtml
hibernate二级缓存攻略
http://www.javaeye.com/topic/18904
Speed Up Your Hibernate Applications with Second-Level Caching
http://www.devx.com/dbzone/Article/29685/1954?pf=true
在现实生活中,在业务层做Cache又会有一些问题
在业务层需要做Cache的方法里要加上添加Cache或清除Cache的代码,这样不但做起来很麻烦,而且把Cache代码和业务逻辑混杂在一起。
在执行一个方法时,哪些关连的Cache需要被清除。如执行了UserBiz.update(userVO)后,需要清除findAll产生的所有Cache,同时也应该把id相同的findById产生的Cache清除。
下面的文章和代码也就是着重解决上面提到的问题
如附图所示,Spring会为所有的Biz方法加上MethodCacheInterceptor.java和 MethodCacheAfterAdvice.java,当方法执行之前,Interceptor会对照Annotation的配置去看此方法的结果需不需要和有没有被Cache,然后决定是否直接从Cache中获得结果(如findAll方法)。而After Advice是在方法执行后决定是否要做一些Cache的清理工作(如update方法)。
具体的Annotation配置方法请参照后面的UserBiz.java
废话和理论还是少说点,上代码才是硬道理
ApplicationContext.xml
Java代码 复制代码
1.
2.
3.
5.
7.
8.
9.
11.
14.
16.
17.
18.
20.
22.
23.
24.
26.
28.
29.
30.
32.
35.
38.
36.
37.
MethodCacheInterceptor.java
Java代码 复制代码
1. package com.novem.common.cache.ehcache;
2.
3. import java.io.Serializable;
4.
5. import net.sf.ehcache.Cache;
6. import net.sf.ehcache.Element;
7.
8. import org.aopalliance.intercept.MethodInterceptor;
9. import org.aopalliance.intercept.MethodInvocation;
10. import org.springframework.beans.factory.InitializingBean;
11. import org.springframework.util.Assert;
12.
13. import com.novem.common.cache.annotation.MethodCache;
14.
15. public class MethodCacheInterceptor implements MethodInterceptor,
16. InitializingBean
17. {
18. private Cache cache;
19.
20. /**
21. * sets cache name to be used
22. */
23. public void setCache(Cache cache)
24. {
25. this.cache = cache;
26. }
27.
28. /**
29. * Checks if required attributes are provided.
30. */
31. public void afterPropertiesSet() throws Exception
32. {
33. Assert.notNull(cache,
34. “A cache is required. Use setCache(Cache) to provide one.”);
35. }
36.
37. /**
38. * main method caches method result if method is configured for caching
39. * method results must be serializable
40. */
41. public Object invoke(MethodInvocation invocation) throws Throwable
42. {
43. // do not need to cache
44. if(!invocation.getMethod().isAnnotationPresent(MethodCache.class)
45. || MethodCache.FALSE.equals(invocation.getMethod().getAnnotation(MethodCache.class).isToCache()))
46. {
47. return invocation.proceed();
48. }
49.
50. String targetName = invocation.getThis().getClass().getName();
51. String methodName = invocation.getMethod().getName();
52. Object[] arguments = invocation.getArguments();
53. Object result;
54.
55. String cacheKey = getCacheKey(targetName, methodName, arguments);
56. Element element = cache.get(cacheKey);
57. if (element == null)
58. {
59. // call target/sub-interceptor
60. result = invocation.proceed();
61.
62. // cache method result
63. element = new Element(cacheKey, (Serializable) result);
64. cache.put(element);
65. }
66. return element.getValue();
67. }
68.
69. /**
70. * creates cache key: targetName.methodName.argument0.argument1…
71. */
72. private String getCacheKey(String targetName, String methodName,
73. Object[] arguments)
74. {
75. StringBuffer sb = new StringBuffer();
76. sb.append(targetName).append(“.”).append(methodName);
77. if ((arguments != null) && (arguments.length != 0))
78. {
79. for (int i = 0; i < arguments.length; i++)
80. {
81. sb.append(".").append(arguments[i]);
82. }
83. }
84.
85. return sb.toString();
86. }
87. }
package com.novem.common.cache.ehcache;
import java.io.Serializable;
import net.sf.ehcache.Cache;
import net.sf.ehcache.Element;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
import com.novem.common.cache.annotation.MethodCache;
public class MethodCacheInterceptor implements MethodInterceptor,
InitializingBean
{
private Cache cache;
/**
* sets cache name to be used
*/
public void setCache(Cache cache)
{
this.cache = cache;
}
/**
* Checks if required attributes are provided.
*/
public void afterPropertiesSet() throws Exception
{
Assert.notNull(cache,
"A cache is required. Use setCache(Cache) to provide one.");
}
/**
* main method caches method result if method is configured for caching
* method results must be serializable
*/
public Object invoke(MethodInvocation invocation) throws Throwable
{
// do not need to cache
if(!invocation.getMethod().isAnnotationPresent(MethodCache.class)
|| MethodCache.FALSE.equals(invocation.getMethod().getAnnotation(MethodCache.class).isToCache()))
{
return invocation.proceed();
}
String targetName = invocation.getThis().getClass().getName();
String methodName = invocation.getMethod().getName();
Object[] arguments = invocation.getArguments();
Object result;
String cacheKey = getCacheKey(targetName, methodName, arguments);
Element element = cache.get(cacheKey);
if (element == null)
{
// call target/sub-interceptor
result = invocation.proceed();
// cache method result
element = new Element(cacheKey, (Serializable) result);
cache.put(element);
}
return element.getValue();
}
/**
* creates cache key: targetName.methodName.argument0.argument1...
*/
private String getCacheKey(String targetName, String methodName,
Object[] arguments)
{
StringBuffer sb = new StringBuffer();
sb.append(targetName).append(".").append(methodName);
if ((arguments != null) && (arguments.length != 0))
{
for (int i = 0; i < arguments.length; i++)
{
sb.append(".").append(arguments[i]);
}
}
return sb.toString();
}
}
MethodCacheAfterAdvice.java
Java代码 复制代码
1. package com.novem.common.cache.ehcache;
2.
3. import java.lang.reflect.Method;
4. import java.util.List;
5.
6. import net.sf.ehcache.Cache;
7.
8. import org.springframework.aop.AfterReturningAdvice;
9. import org.springframework.beans.factory.InitializingBean;
10. import org.springframework.util.Assert;
11.
12. import com.novem.common.cache.annotation.CacheCleanMethod;
13. import com.novem.common.cache.annotation.MethodCache;
14.
15. public class MethodCacheAfterAdvice implements AfterReturningAdvice,
16. InitializingBean
17. {
18. private Cache cache;
19.
20. public void setCache(Cache cache)
21. {
22. this.cache = cache;
23. }
24.
25. public MethodCacheAfterAdvice()
26. {
27. super();
28. }
29.
30. public void afterReturning(Object returnValue, Method method,
31. Object[] args, Object target) throws Throwable
32. {
33. // do not need to remove cache
34. if (!method.isAnnotationPresent(MethodCache.class)
35. || method.getAnnotation(MethodCache.class).cacheCleanMethods().length == 0)
36. {
37. return;
38. }
39. else
40. {
41. String targetName = target.getClass().getName();
42.
43. CacheCleanMethod[] cleanMethods = method.getAnnotation(
44. MethodCache.class).cacheCleanMethods();
45. List list = cache.getKeys();
46. for (int i = 0; i < list.size(); i++)
47. {
48. for (int j = 0; j < cleanMethods.length; j++)
49. {
50. String cacheKey = String.valueOf(list.get(i));
51.
52. StringBuffer tempKey = new StringBuffer();
53. tempKey.append(targetName);
54. tempKey.append(".");
55. tempKey.append(cleanMethods[j].methodName());
56.
57. if (CacheCleanMethod.CLEAN_BY_ID.equals(cleanMethods[j].cleanType()))
58. {
59. tempKey.append(".");
60. tempKey.append(getIdValue(target, method, args[0]));
61. }
62.
63. if (cacheKey.startsWith(tempKey.toString()))
64. {
65. cache.remove(cacheKey);
66. }
67. }
68. }
69. }
70. }
71.
72. private String getIdValue(Object target, Method method, Object idContainer)
73. {
74. String targetName = target.getClass().getName();
75.
76. // get id value
77. String idValue = null;
78. if (MethodCache.TRUE.equals(method.getAnnotation(MethodCache.class)
79. .firstArgIsIdContainer()))
80. {
81. if (idContainer == null)
82. {
83. throw new RuntimeException(
84. "Id container cannot be null for method "
85. + method.getName() + " of " + targetName);
86. }
87.
88. Object id = null;
89. try
90. {
91. Method getIdMethod = idContainer.getClass().getMethod("getId");
92. id = getIdMethod.invoke(idContainer);
93. }
94. catch (Exception e)
95. {
96. throw new RuntimeException("There is no getId method for "
97. + idContainer.getClass().getName());
98. }
99.
100. if (id == null)
101. {
102. throw new RuntimeException("Id cannot be null for method "
103. + method.getName() + " of " + targetName);
104. }
105. idValue = id.toString();
106. }
107. else if (MethodCache.TRUE.equals(method
108. .getAnnotation(MethodCache.class).firstArgIsId()))
109. {
110. if (idContainer == null)
111. {
112. throw new RuntimeException("Id cannot be null for method "
113. + method.getName() + " of " + targetName);
114. }
115. idValue = idContainer.toString();
116. }
117.
118. return idValue;
119. }
120.
121. public void afterPropertiesSet() throws Exception
122. {
123. Assert.notNull(cache,
124. "Need a cache. Please use setCache(Cache) create it.");
125. }
126.
127. }
package com.novem.common.cache.ehcache;
import java.lang.reflect.Method;
import java.util.List;
import net.sf.ehcache.Cache;
import org.springframework.aop.AfterReturningAdvice;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
import com.novem.common.cache.annotation.CacheCleanMethod;
import com.novem.common.cache.annotation.MethodCache;
public class MethodCacheAfterAdvice implements AfterReturningAdvice,
InitializingBean
{
private Cache cache;
public void setCache(Cache cache)
{
this.cache = cache;
}
public MethodCacheAfterAdvice()
{
super();
}
public void afterReturning(Object returnValue, Method method,
Object[] args, Object target) throws Throwable
{
// do not need to remove cache
if (!method.isAnnotationPresent(MethodCache.class)
|| method.getAnnotation(MethodCache.class).cacheCleanMethods().length == 0)
{
return;
}
else
{
String targetName = target.getClass().getName();
CacheCleanMethod[] cleanMethods = method.getAnnotation(
MethodCache.class).cacheCleanMethods();
List list = cache.getKeys();
for (int i = 0; i < list.size(); i++)
{
for (int j = 0; j < cleanMethods.length; j++)
{
String cacheKey = String.valueOf(list.get(i));
StringBuffer tempKey = new StringBuffer();
tempKey.append(targetName);
tempKey.append(".");
tempKey.append(cleanMethods[j].methodName());
if (CacheCleanMethod.CLEAN_BY_ID.equals(cleanMethods[j].cleanType()))
{
tempKey.append(".");
tempKey.append(getIdValue(target, method, args[0]));
}
if (cacheKey.startsWith(tempKey.toString()))
{
cache.remove(cacheKey);
}
}
}
}
}
private String getIdValue(Object target, Method method, Object idContainer)
{
String targetName = target.getClass().getName();
// get id value
String idValue = null;
if (MethodCache.TRUE.equals(method.getAnnotation(MethodCache.class)
.firstArgIsIdContainer()))
{
if (idContainer == null)
{
throw new RuntimeException(
"Id container cannot be null for method "
+ method.getName() + " of " + targetName);
}
Object id = null;
try
{
Method getIdMethod = idContainer.getClass().getMethod("getId");
id = getIdMethod.invoke(idContainer);
}
catch (Exception e)
{
throw new RuntimeException("There is no getId method for "
+ idContainer.getClass().getName());
}
if (id == null)
{
throw new RuntimeException("Id cannot be null for method "
+ method.getName() + " of " + targetName);
}
idValue = id.toString();
}
else if (MethodCache.TRUE.equals(method
.getAnnotation(MethodCache.class).firstArgIsId()))
{
if (idContainer == null)
{
throw new RuntimeException("Id cannot be null for method "
+ method.getName() + " of " + targetName);
}
idValue = idContainer.toString();
}
return idValue;
}
public void afterPropertiesSet() throws Exception
{
Assert.notNull(cache,
"Need a cache. Please use setCache(Cache) create it.");
}
}
MethodCache.java
Java代码 复制代码
1. package com.novem.common.cache.annotation;
2.
3. import java.lang.annotation.Retention;
4. import java.lang.annotation.RetentionPolicy;
5.
6. @Retention(RetentionPolicy.RUNTIME)
7. public @interface MethodCache
8. {
9. String TO_CACHE = "TO_CACHE";
10. String NOT_TO_CACHE = "NOT_TO_CACHE";
11.
12. String TRUE = "TRUE";
13. String FALSE = "FALSE";
14.
15. public String isToCache() default TO_CACHE;
16.
17. public String firstArgIsId() default FALSE;
18.
19. public String firstArgIsIdContainer() default FALSE;
20.
21. public CacheCleanMethod[] cacheCleanMethods() default {};
22. }
package com.novem.common.cache.annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
public @interface MethodCache
{
String TO_CACHE = "TO_CACHE";
String NOT_TO_CACHE = "NOT_TO_CACHE";
String TRUE = "TRUE";
String FALSE = "FALSE";
public String isToCache() default TO_CACHE;
public String firstArgIsId() default FALSE;
public String firstArgIsIdContainer() default FALSE;
public CacheCleanMethod[] cacheCleanMethods() default {};
}
CacheCleanMethod.java
Java代码 复制代码
1. package com.novem.common.cache.annotation;
2.
3. import java.lang.annotation.Retention;
4. import java.lang.annotation.RetentionPolicy;
5.
6. @Retention(RetentionPolicy.RUNTIME)
7. public @interface CacheCleanMethod
8. {
9. String CLEAN_ALL = "CLEAN_ALL";
10. String CLEAN_BY_ID = "CLEAN_BY_ID";
11.
12. public String methodName();
13.
14. public String cleanType() default CLEAN_ALL;
15. }
package com.novem.common.cache.annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
public @interface CacheCleanMethod
{
String CLEAN_ALL = "CLEAN_ALL";
String CLEAN_BY_ID = "CLEAN_BY_ID";
public String methodName();
public String cleanType() default CLEAN_ALL;
}
UserBiz.java
Java代码 复制代码
1. package com.novem.farc.biz;
2.
3. import java.util.List;
4.
5. import com.novem.common.cache.annotation.CacheCleanMethod;
6. import com.novem.common.cache.annotation.MethodCache;
7. import com.novem.farc.vo.UserVO;
8.
9. public interface UserBiz
10. {
11. @MethodCache()
12. public UserVO findById(Long id);
13.
14. @MethodCache()
15. public List findAll(int firstResult, int maxResults);
16.
17. @MethodCache(
18. isToCache = MethodCache.FALSE,
19. firstArgIsIdContainer = MethodCache.TRUE,
20. cacheCleanMethods = {@CacheCleanMethod(methodName="findById", cleanType = CacheCleanMethod.CLEAN_BY_ID),
21. @CacheCleanMethod(methodName="findAll")}
22. )
23. public void update(UserVO vo);
24.
25. @MethodCache(
26. isToCache = MethodCache.FALSE,
27. firstArgIsIdContainer = MethodCache.TRUE,
28. cacheCleanMethods = {@CacheCleanMethod(methodName="findAll")}
29. )
30. public Long insert(UserVO vo);
31.
32. @MethodCache(
33. isToCache = MethodCache.FALSE,
34. firstArgIsId = MethodCache.TRUE,
35. cacheCleanMethods = {@CacheCleanMethod(methodName="findById", cleanType = CacheCleanMethod.CLEAN_BY_ID),
36. @CacheCleanMethod(methodName="findAll")}
37. )
38. public void remove(Long id);
39. }
package com.novem.farc.biz;
import java.util.List;
import com.novem.common.cache.annotation.CacheCleanMethod;
import com.novem.common.cache.annotation.MethodCache;
import com.novem.farc.vo.UserVO;
public interface UserBiz
{
@MethodCache()
public UserVO findById(Long id);
@MethodCache()
public List findAll(int firstResult, int maxResults);
@MethodCache(
isToCache = MethodCache.FALSE,
firstArgIsIdContainer = MethodCache.TRUE,
cacheCleanMethods = {@CacheCleanMethod(methodName="findById", cleanType = CacheCleanMethod.CLEAN_BY_ID),
@CacheCleanMethod(methodName="findAll")}
)
public void update(UserVO vo);
@MethodCache(
isToCache = MethodCache.FALSE,
firstArgIsIdContainer = MethodCache.TRUE,
cacheCleanMethods = {@CacheCleanMethod(methodName="findAll")}
)
public Long insert(UserVO vo);
@MethodCache(
isToCache = MethodCache.FALSE,
firstArgIsId = MethodCache.TRUE,
cacheCleanMethods = {@CacheCleanMethod(methodName="findById", cleanType = CacheCleanMethod.CLEAN_BY_ID),
@CacheCleanMethod(methodName="findAll")}
)
public void remove(Long id);
}
注意:如果@CacheCleanMethod的cleanType = CacheCleanMethod.CLEAN_BY_ID,则此方法的第一个参数一定要是对象的ID(userId)或ID container(UserVO, 并且此对象中要有getId方法)。之所以要有这样的限制,我是觉得在企业开发中,大家follow这样的规则就好,没必要为了能灵活地取出ID再多搞出一些配置项出来。
为什么我不使用XML来配置Cache呢?
下面是我最早的时候写的一个配置XML,但后来发现,使用这种方法,就得为每一个Biz配置一个XML,就和早期的xxx.hbm.xml一样,管理起来比较麻烦,不如Annotation简洁
UserBiz.cache.xml
Java代码 复制代码
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
Flex+Java中小型项目的代码研究
相关推荐
- 员工考勤打卡时,如何避免非本人代替打卡? - 华为云开发者联盟 - 博客园
- Web Components从技术解析到生态应用个人心得指北 - zhoulujun - 博客园
- 【经典问题】mysql和redis数据一致性问题 - Scotyzh - 博客园
- vs出现错误,无法启动 Visual Studio。StreamJsonRpc.ConnectionLostException:在请求完成之前,与远程的JSON-RPC连接已丢失_客服专区-CSDN问答
- 【转】Chrome内核浏览器打开网页报 错误代码: ERR_TIMED_OUT - m_lm的个人空间 - OSCHINA - 中文开源技术交流社区
- ASP.NET Core WebApi配置跨域_asp.net core webapi 跨域-CSDN博客
- C# 怎么用OpenCVSharp4实现图片表格识别
- ChatGPT 本地部署及搭建_孟郎郎的博客-CSDN博客