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)) {

多快好省!前端后端,线上线下,名师精讲
更多了解 加: