Spring本身不直接提供缓存功能的实现,但提供了对缓存功能的抽象:
public interface CacheManager {
public interface Cache {
演示:Spring自带了上述接口的实现EhCacheCacheManager和EhCacheCache
public class EhCacheCacheManager extends AbstractTransactionSupportingCacheManager {
public class EhCacheCache implements Cache注意以上接口/实现类的package:
package org.springframework.cache; package org.springframework.cache.ehcache;
他们都不是真正的ehcache实现。
首先需要maven引入(复习:Hibernate二级缓存)
然后用在springmvc-servlet.xml中配置CacheManager的SpringBean为:
<bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager"> <property name="cacheManager" ref="ehcache"/> </bean>注意其中的ref,实际上指向的就是:
<bean id="ehcache" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"> <property name="configLocation" value="/WEB-INF/ehcache.xml"/> </bean>EhCacheManagerFactoryBean才利用ehcache.xml文件真正生成EhCacheManager……
PS:Ehcache的CacheManager构造函数或工厂方法被调用时,会默认加载classpath下名为ehcache.xml的配置文件。如果加载失败,会加载Ehcache jar包中的ehcache-failsafe.xml(演示)文件,这个文件中含有简单的默认配置。
ehcache.xml配置
<cache name="user" maxElementsInMemory="1000" eternal="false" timeToIdleSeconds="50" timeToLiveSeconds="50" overflowToDisk="false" memoryStoreEvictionPolicy="LRU"/>
参数说明:
通过依赖注入获取CacheManager对象:
@Autowired private CacheManager cacheManager;
将CacheManager对象转换成EhCacheCacheManager对象(必须的,不要忘记):
EhCacheCacheManager ecm = (EhCacheCacheManager)cacheManager;然后,通过name获取Cache对象:
//Collection<String> cacheNames = ecm.getCacheNames(); Cache cache = ecm.getCache("user");
cache中的元素是以键值对的形式存储的,可以通过其get()方法获取,put()方法存入:
String key = "time"; ValueWrapper valueWrapper = cache.get(key); if (valueWrapper == null) { cache.put(key, LocalDateTime.now()); }else { System.out.println((LocalDateTime)valueWrapper.get()); }
注意:Spring中Cache对象get()返回的还是一个ValueWrapper对象(为了提供一套对外一致的API),还需要再get() 一次才能获取到真正的缓存值,且类型为Object。也可以使用泛型方法,指定缓存对象类型:
LocalDateTime time = cache.get(key, LocalDateTime.class);
以上方式,通常用于缓存从数据库中获取的对象。但我们的项目已使用Hibernate的二级缓存技术,所以再API缓存的意义不大……
<cache:annotation-driven cache-manager="cacheManager" proxy-target-class="true"/>
说明:
PS:附带的xmlns
xmlns:cache="http://www.springframework.org/schema/cache"和xsi:schemaLocation
http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd
在一个(public)方法上添加注解@Cacheable,就可以把该方法参数和返回结果作为一个键值对存放在缓存中。下次利用同样的参数来调用该方法时将不再执行该方法,而是直接从缓存中获取结果进行返回。
@Cacheable(cacheNames = "user", key = "#root.methodName") protected LoginStatusModel setLoginStatus() {
public class Cache { static class Names{ final static String Article = "article";
Spring Expression Language,Spring表达式语言。
可以简单的理解成Spring可理解可解析的一种语言表达形式,常用于Cache注解的包括:
public class Cache { static class Keys{ final static String Prefix = "#root.targetClass.Name+'-'+#root.methodName+'-'+";为了调试或追踪缓存运行情况,有时候需要
<logger name="net.sf.ehcache" level="all"> <AppenderRef ref="FileSQL" /> </logger> <logger name="org.springframework.cache" level="all"> <AppenderRef ref="FileSQL" /> </logger>#体会:默认使用包名做logger……#
演示:@Cacheable生效,但
Adding cacheable method 'setLoginStatus' with attribute:
被添加了n次,因为setLoginStatus()是基类方法,被n个子类继承……
不要作用于(返回ModelAndView或String的)handler method!
@想一想@:为什么?
演示:给model添加attribute的代码不会执行
@Cacheable(cacheNames = "user" , key = "#root.methodName" ) public ModelAndView on(Model model) {
理论上可以省略@Cacheable中的属性key声明,但是这样的话就会直接使用Spring默认的SimpleKeyGenerator:
public class SimpleKeyGenerator implements KeyGenerator { @Override public Object generate(Object target, Method method, Object... params) { return generateKey(params);比较坑爹: 不管调用方法的对象(target)和方法(method)的……所以任何方法只要有相同的参数(param)就会生成相同的key?!
所以,我们一般都使用自定义的KeyGenerator :
@Component(Cache.KeyGenerator.Name) public class FullCacheKeyGenerator extends SimpleKeyGenerator { @Override public Object generate(Object target, Method method, Object... params) { String paramsKeyPart = null; return String.join("-", target.getClass().getName(), method.getName() , paramsKeyPart);@Component(Cache.KeyGenerator.Name)中指定的字符串可用于:
public class Cache { static class KeyGenerator{ final static String Name = "fullCacheKeyGenerator";如何利用利用params生成相应的key,实际开发中有很多种方式。这里出于简单展示的目的,可以利用其基类的方法:
if (params.length > 0) { if (params.length == 1) { paramsKeyPart = String.valueOf(super.generateKey(params).hashCode()); }else { paramsKeyPart = ((SimpleKey)(super.generateKey(params))).toString(); } }//else nothing
演示:修改文章后仍然显示缓存的过期数据
解决方案:利用@CacheEvict,每当文章被修改,就删除缓存数据。
@CacheEvict(cacheNames = Cache.Names.Article, keyGenerator = Cache.KeyGenerator.Name, condition = "#root.target.canEvict(#p0)", beforeInvocation = true) public SingleModel GetById(int id) {需要设置一个条件(condition),否则每次调用都会删除该方法对应的缓存。condition中使用的仍然是SpEL,调用了当前对象的方法:
public boolean canEvict(int id) { return editedArticleIds.contains(id);editedArticleIds是一个静态字段,记录所有应该被evict的文章id:
static Set<Integer> editedArticleIds = new HashSet<>();哪些文章id应该被加入/移出editedArticleIds呢?
public SingleModel GetById(int id) { editedArticleIds.remove(id);
public void Edit(NewOrEditModel model, int id) { editedArticleIds.add(id);
@想一想@:能不能使用API直接删除某条缓存数据?
PS:其他cache相关注解@CachePut、@Cacheable、@CacheConfig..等略
@Async public void sendEmail() { System.out.println("send email ……");
它的本质是依赖于多线程,即:对注解了@Async的方法另开一个线程予以执行。
要让异步生效,还需要在中开启:
<task:executor id="asynExecutor" pool-size="5" /> <task:annotation-driven executor="asynExecutor" />
xmlns:task="http://www.springframework.org/schema/task"
http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-4.0.xsd
对比演示:
public ModelAndView input( userService.sendEmail(); System.out.println("after userService.sendEmail()");
System.out.println("currentThread() in xxx:" + Thread.currentThread().getId());
诸如发送email、log记录等和HTTP响应无关,不需要返回值的方法是最适合使用@Async注解的。
一些极其特殊的情况,我们可能需要异步获得方法的返回值,这时候:
@Async public CompletableFuture<Boolean> sendEmail() { return CompletableFuture.supplyAsync(()->true);
userService.sendEmail().thenAccept(r->System.out.println(r));
但是,在我们SessionPerRequest的架构中,不要在异步方法中通过绑定request声明周期的session/entityManager获取数据库的值。因为当异步方法执行时,很可能request已经被释放!演示:
Error creating bean with name 'scopedTarget.emWrapper': Scope 'request' is not active for the current thread;
Ajax请求仍然是一个HTTP请求,如果要求的响应是HTML片段的话,后台的处理和“响应一个完整的HTML页面”没有区别。比如通过Ajax加载消息栏内容:
<p b-notification></p> <script> $(document).ready(function () { $('[b-notification]').load("/17bang/Notification/GetRandom"); }) </script>
后台一样用MVC模式,将/Notification/GetRandom请求dispatch到NotificationController的GetRandom()方法而已……
@Controller @RequestMapping(URLMapping.Notification.Base) public class NotificationController extends BaseController { @RequestMapping(URLMapping.Notification.GetRandom) public String GetRandom() { //这里你就可以为所欲为啦! return URLMapping.Notification.Base + URLMapping.Notification.GetRandom;
对SpringMVC而言,响应Ajax请求只有一个新知识点:
首先需要在handler method上添加@ResponseBody注解(复习:动态文件输出)
@ResponseBody public boolean GetRandom() { //用boolean表示是否有新消息……演示:500错误,没有converter
PS:使用String作为返回值解决不了问题,因为String格式的false仍然会被认为是true:
$.get("/17bang/Notification/GetRandom", function(result){ if(result){ //"false" => true $('[b-notification]').text("有新消息啦!"); } });
所以需要使用maven引入jackson(注意版本和tomcat匹配):
<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.9.7</version> </dependency>
无论是基本类型数据(比如int、boolean……)还是自定义类型,都可以通过jackson进行转化:
public MineModel GetRandom() { return new MessageService().Get();
PS:请求为JSON格式的场景在SpringBoot中演示错误处理
Ajax请求中出现错误,就不宜再进行页面跳转,而是应该返回一些错误信息。否则无法在前台击中Ajax的error handler:
$.ajax({ //...以上略 error : function(jqXHR, textStatus, errorThrown) { console.log(errorThrown); } })F12演示:302状态码……
可以使用两种方式:
response.sendError(500, "系统错误……");
mv.setStatus(HttpStatus.INTERNAL_SERVER_ERROR);
演示:
但上述方法不能很方便的给予详细的错误消息(比如status和message)。所以有时候项目/架构会要求用JSON数据返回错误信息,这时候根据请求返回的类型,如果:
public MineModel GetRandom() {可以让Model都继承自BaseModel,
public class MineModel extends BaseModel {在BaseModel中添加status和message,
public class BaseModel { private String status = "success"; private String message = "OK";最后在catch中设置
model.setStatus("fail"); model.setMessage("系统错误,巴拉巴拉……");
public ModelAndView GetRandom() {需要“凭空生成”一个view,
MappingJackson2JsonView view = new MappingJackson2JsonView(); mv.setView(view);这样model中的值就可以作为JSON内容返回给前端了
mv.addObject("status", "fail"); mv.addObject("message", "系统错误……");
$.ajax({ success: function(result){ if(result.status == "fail"){ console.log(result.message); }else{如果是在@ExceptionHandler方法中统一处理,就需要首先判断一个请求
根据请求头中x-requested-with的值是否为XMLHttpRequest确定:
public static boolean isAjaxRequest(HttpServletRequest request) { String requestedWith = request.getHeader("x-requested-with"); return requestedWith != null && requestedWith.equalsIgnoreCase("XMLHttpRequest"); }
然后就可以在@ExceptionHandler方法中:
if (isAjaxRequest(request)) {
多快好省!前端后端,线上线下,名师精讲
更多了解 加: