Flex+Java中小型项目的代码研究

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. 4. classpath:ehcache.xml
5.
6.

7.
8.
9. 10.
11.
12. 13. com.novem.common.cache.ehcache.METHOD_CACHE
14.
15.

16.
17.
18. 19.
20.
21.

22.
23.
24. 25.
26.
27.

28.
29.
30. 31. *Biz
32.
33. 34. 35. methodCacheInterceptor
36. methodCacheAfterAdvice
37.
38.
39.


classpath:ehcache.xml
com.novem.common.cache.ehcache.METHOD_CACHE


*Biz methodCacheInterceptor
methodCacheAfterAdvice

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.

赞(0) 打赏
分享到: 更多 (0)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏