朱晔和你聊Spring系列S1E4:灵活但不算好用的Spring MVC
本文会以一些例子来展现Spring MVC的常见功能和一些扩展点,然后我们来讨论一下Spring MVC好用不好用。
使用SpringBoot快速开始
基于之前的parent模块,我们来创建一个新的模块:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>me.josephzhu</groupId>
<artifactId>spring101-webmvc</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>spring101-webmvc</name>
<description></description>
<parent>
<groupId>me.josephzhu</groupId>
<artifactId>spring101</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
使用web来启用Spring MVC,使用thymeleaf来启用thymeleaf模板引擎。Thymeleaf是一个强大的Java模板引擎,可以脱离于Web单独使用,本身就有非常多的可配置可扩展的点,这里不展开讨论,详见官网。
接下去我们创建主程序:
package me.josephzhu.spring101webmvc;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Spring101WebmvcApplication {
public static void main(String[] args) {
SpringApplication.run(Spring101WebmvcApplication.class, args);
}
}
以及一个测试Controller:
package me.josephzhu.spring101webmvc;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.servlet.ModelAndView;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
@Controller
public class MyController {
@GetMapping("shop")
public ModelAndView shop() {
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("shop");
modelAndView.addObject("items",
IntStream.range(1, 5)
.mapToObj(i -> new MyItem("item" + i, i * 100))
.collect(Collectors.toList()));
return modelAndView;
}
}
这里使用到了一个自定义的类:
package me.josephzhu.spring101webmvc;
import lombok.AllArgsConstructor;
import lombok.Data;
@AllArgsConstructor
@Data
public class MyItem {
private String name;
private Integer price;
}
最后我们需要在resources目录下创建一个templates目录,在目录下再创建一个shop.html模板文件:
<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-4.dtd">
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hello Shop</title>
</head>
<body>
Hello Shop
<table>
<tr th:each="item : ${items}">
<td th:text="${item.name}">...</td>
<td th:text="${item.price}">...</td>
</tr>
</table>
</body>
</html>
我们看到有了SpringBoot,创建一个Spring MVC程序整个过程非常简单:
1 引入starter
2 创建@Controller,设置@RequestMapping
3 创建模板文件
没有任何配置工作,一切都是starter自动配置。
快速配置ViewController
几乎所有Spring MVC的扩展点都集成在了接口中,要进行扩展很简单,实现这个接口,加上@Configuration和@EnableWebMvc注解,实现需要的方法即可。
我们先用它来快速配置一些ViewController:
package me.josephzhu.spring101webmvc;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.http.HttpStatus;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.*;
import org.springframework.web.servlet.resource.GzipResourceResolver;
import org.springframework.web.servlet.resource.VersionResourceResolver;
import java.util.List;
@EnableWebMvc
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/hello").setViewName("helloworld");
registry.addRedirectViewController("/", "/hello");
registry.addStatusController("/user", HttpStatus.BAD_REQUEST);
}
}
代码中多贴了一些后面会用到的import在这里可以忽略。这里我们配置了三套策略:
1 访问/会跳转到/hello
2 访问/hello会访问helloworld这个view
3 访问/user会给出400的错误代码
这里我们在templats目录再添加一个空白的helloworld.html:
<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-4.dtd">
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hello World</title>
</head>
<body>
Hello World
</body>
</html>
这种配置方式可以省一些代码量,但是我个人认为在这里做配置可读性一般。
定制路径匹配
我们还可以实现路径匹配策略的定制:
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.setUseTrailingSlashMatch(false);
}
比如这样就关闭了结尾为/的匹配(默认开启)。试着访问http://localhost:8080/shop/得到如下错误:
2018-10-02 18:58:16.581 WARN 20264 --- [nio-8080-exec-1] o.s.web.servlet.PageNotFound : No mapping found for HTTP request with URI [/shop/] in DispatcherServlet with name 'dispatcherServlet'
这个方法可以针对路径匹配进行相当多的配置,具体请参见文档,这里只列出了其中的一个功能。
配置静态资源
在配置类加上下面的代码:
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**")
.addResourceLocations("classpath:/static/")
.resourceChain(true)
.addResolver(new GzipResourceResolver())
.addResolver(new VersionResourceResolver()
.addFixedVersionStrategy("1.0.0", "/**"));
}
这就实现了静态资源路由到static目录,并且为静态资源启用了Gzip压缩和基于版本号的缓存。配置后我们在resources目录下创建一个static目录,然后随便创建一个a.html文件,试试访问这个文件,测试可以发现:http://localhost:8080/static/1.0.0/a.html和http://localhost:8080/static/a.html都可以访问到这个文件。
解析自定义的参数
HandlerMethodArgumentResolver接口这是一个非常非常重要常用的扩展点。通过这个接口,我们可以实现通用方法来装配HandlerMethod上的自定义参数,我们现在来定义一个MyDevice类型,然后我们希望框架可以在所有出现MyDevice参数的时候自动为我们从Header里获取相应的设备信息构成MyDevice对象(如果我们API的使用者是客户端应用程序,这是不是一个挺常见的需求)。
package me.josephzhu.spring101webmvc;
import lombok.Data;
@Data
public class MyDevice {
private String type;
private String version;
private String screen;
}
然后是自定义的HandlerMethodArgumentResolver实现:
package me.josephzhu.spring101webmvc;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
public class DeviceHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter methodParameter) {
return methodParameter.getParameterType().equals(MyDevice.class);
}
@Override
public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
MyDevice myDevice = new MyDevice();
myDevice.setType(nativeWebRequest.getHeader("device.type"));
myDevice.setVersion(nativeWebRequest.getHeader("device.version"));
myDevice.setScreen(nativeWebRequest.getHeader("device.screen"));
return myDevice;
}
}
实现分两部分,第一部分告诉框架,我们这个ArgumentResolver支持解析怎么样的参数。这里我们的实现是根据参数类型,还有很多时候可以通过检查是否参数上有额外的自定义注解来实现(后面也会有例子)。第二部分就是真正的实现了,实现非常简单,从请求头里获取相应的信息构成我们的MyDevice对象。
要让这个Resolver被MVC框架识别到,我们需要继续扩展刚才的WebConfig类,加入下面的代码:
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new DeviceHandlerMethodArgumentResolver());
}
然后,我们写一个例子来测试一下:
package me.josephzhu.spring101webmvc;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
@RestController
@Slf4j
@RequestMapping("api")
public class MyRestController {
@RequestMapping(value = "items", method = RequestMethod.GET)
public List<MyItem> getItems(MyDevice device) {
log.debug("Device : " + device);
List<MyItem> myItems = new ArrayList<>();
myItems.add(new MyItem("aa", 10));
myItems.add(new MyItem("bb", 20));
return myItems;
}
}
这里因为用了debug,所以需要在配置文件中打开debug日志级别:
logging.level.me.josephzhu.spring101webmvc=DEBUG
测试一下:
curl -X GET \
http://localhost:8080/api/items \
-H 'device.screen: 1280*800' \
-H 'device.type: android' \
-H 'device.version: 1.1'
可以在控制台看到这样的日志:
2018-10-02 19:10:56.667 DEBUG 20325 --- [nio-8080-exec-9] m.j.spring101webmvc.MyRestController : Device : MyDevice(type=android, version=1.1, screen=1280*800)
可以证明我们方法中定义的MyDevice的确是从请求中获取到了正确的结果。大家可以发挥一下想象,ArgumentResolver不但可以做类似参数自动装配(从各个地方获取必要的数据)的工作,而且还可以做验证工作。大家可以仔细看一下resolveArgument方法的参数,是不是相当于要啥有啥了(当前参数定义、当前请求、Model容器以及绑定工厂)。
自定义ResponseBody后处理
在刚才的实现中,我们直接返回了List数据,对于API来说,我们一般会定义一套API的结果对象,包含API的数据、成功与否结果、错误消息、签名等等内容,这样客户端可以做签名验证,然后是根据成功与否来决定是要解析数据还是直接提示错误,比如:
package me.josephzhu.spring101webmvc;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class APIResponse<T> {
T data;
boolean success;
String message;
String sign;
}
如果我们在每个API方法中去返回这样的APIResponse当然可以实现这个效果,还有一种通用的实现方式是使用ResponseBodyAdvice:
package me.josephzhu.spring101webmvc;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
@ControllerAdvice
public class APIResponseBodyAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return !returnType.getParameterType().equals(APIResponse.class);
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
String sign = "";
Sign signAnnotation = returnType.getMethod().getAnnotation(Sign.class);
if (signAnnotation != null)
sign = "abcd";
return new APIResponse(body, true, "", sign);
}
}
通过定义@ControllerAdvice注解来启用这个Advice。在实现上也是两部分,第一部分告诉框架我们这个Advice支持的是非APIResponse类型(如果返回的对象已经是APIResponse了,我们当然就不需要再包装一次了)。第二部分是实现,这里的实现很简单,我们先检查一下方法上是否有Sign这个注解,如果有的话进行签名(这里的逻辑是写死的签名),然后把得到的body塞入APIResponse后返回。
这里补上Sign注解的实现:
package me.josephzhu.spring101webmvc;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Sign {
}
这是一个空注解,没啥可以说的,下面我们来测试一下这个ResponseBodyAdvice:
@RequestMapping(value = "item/{id}", method = RequestMethod.GET)
public MyItem getItem(@PathVariable("id") String id) {
Integer i = null;
try {
i = Integer.parseInt(id);
} catch (NumberFormatException ex) {
}
if (i == null || i < 1)
throw new IllegalArgumentException("不合法的商品ID");
return new MyItem("item" + id, 10);
}
访问http://localhost:8080/api/item/23后得到如下图的结果:

是不是很方便呢?这个API包装的过程可以由框架进行,无需每次手动来做。
自定义异常处理
如果我们访问http://localhost:8080/api/item/0会看到错误白页,针对错误处理,我们希望:
1 可以使用统一的APIResponse方式进行错误返回
2 可以记录错误信息以便查看
实现这个功能非常简单,我们可以通过@ExceptionHandler实现:
package me.josephzhu.spring101webmvc;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.method.HandlerMethod;
import javax.servlet.http.HttpServletRequest;
@ControllerAdvice(annotations = RestController.class)
@Slf4j
public class MyRestExceptionHandler {
@ExceptionHandler
@ResponseBody
public APIResponse handle(HttpServletRequest req, HandlerMethod method, Exception ex) {
log.error(String.format("访问 %s -> %s 出错了!", req.getRequestURI(), method.toString()), ex);
return new APIResponse(null, false, ex.getMessage(), "");
}
}
注意几点:
1 我们可以使用@ControllerAdvice的annotations来关联我们需要拦截的Controller类型
2 handle方法支持相当多的参数,可谓是要啥有啥,这里贴下官方文档说明的截图(在这里我们使用了ServletRequest来获取请求地址,使用了HandlerMethod来获取当前执行的方法):

访问地址http://localhost:8080/api/item/sd可以看到如下输出:

(注意,处理签名的ResponseBodyAdvice并不会针对这个返回进行处理,因为之前实现的时候我们就判断了返回内容不是APIResponse才去处理,在自己正式的实现中你可以实现的更合理,让签名的处理逻辑同时适用出现异常的情况)日志中也出现了错误信息:
2018-10-02 19:48:41.450 ERROR 20422 --- [nio-8080-exec-6] m.j.s.MyRestExceptionHandler : 访问 /api/item/sd -> public me.josephzhu.spring101webmvc.MyItem me.josephzhu.spring101webmvc.MyRestController.getItem(java.lang.String) 出错了!
java.lang.IllegalArgumentException: 不合法的商品ID
at me.josephzhu.spring101webmvc.MyRestController.getItem(MyRestController.java:34) ~[classes/:na]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_161]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_161]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_161]
at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_161]
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:209) ~[spring-web-5.0.9.RELEASE.jar:5.0.9.RELEASE]
自动处理参数类型转换
比如有这么一个需求,我们希望可以接受自定义的枚举作为参数,而且枚举的名字不一定需要和请求的参数完全大小写匹配,这个时候我们需要实现自己的转换器:
package me.josephzhu.spring101webmvc;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory;
import java.util.Arrays;
public class MyConverterFactory implements ConverterFactory<String, Enum> {
@Override
public <T extends Enum> Converter<String, T> getConverter(Class<T> targetType) {
return new String2EnumConverter(targetType);
}
class String2EnumConverter<T extends Enum<T>> implements Converter<String, T> {
private Class<T> enumType;
private String2EnumConverter(Class<T> enumType) {
this.enumType = enumType;
}
@Override
public T convert(String source) {
return Arrays.stream(enumType.getEnumConstants())
.filter(e -> e.name().equalsIgnoreCase(source))
.findAny().orElse(null);
}
}
}
这里实现了一个从字符串到自定义枚举的转换,在搜索枚举名字的时候我们忽略了大小写。
接下去我们通过WebConfig来注册这个转换器工厂:
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverterFactory(new MyConverterFactory());
}
来写一段代码测试一下:
@GetMapping("search")
public List<MyItem> search(@RequestParam("type") ItemTypeEnum itemTypeEnum) {
return IntStream.range(1, 5)
.mapToObj(i -> new MyItem(itemTypeEnum.name() + i, i * 100))
.collect(Collectors.toList());
}
这是一个Get请求的API,接受一个type参数,参数是一个自定义枚举:
package me.josephzhu.spring101webmvc;
public enum ItemTypeEnum {
BOOK, TOY, TOOL
}
很明显枚举的名字都是大写的,我们来访问一下地址http://localhost:8080/api/search?type=TOy 测试一下程序是否可以正确匹配:

TOy的搜索参数匹配到了TOY枚举,结果符合我们的预期。
自定义拦截器
最后,我们来看看Spring MVC最通用的扩展点,也就是拦截器。

这个图清晰展现了拦截器几个重要方法事件节点。在这个例子中,我们利用preHandle和postHandle两个方法实现可以统计请求执行耗时的拦截器:
package me.josephzhu.spring101webmvc;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Slf4j
public class ExecutionTimeHandlerInterceptor extends HandlerInterceptorAdapter {
private static final String START_TIME_ATTR_NAME = "startTime";
private static final String EXECUTION_TIME_ATTR_NAME = "executionTime";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
long startTime = System.currentTimeMillis();
request.setAttribute(START_TIME_ATTR_NAME, startTime);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
long startTime = (Long) request.getAttribute(START_TIME_ATTR_NAME);
long endTime = System.currentTimeMillis();
long executionTime = endTime - startTime;
String time = "[" + handler + "] executeTime : " + executionTime + "ms";
if (modelAndView != null) {
modelAndView.addObject(EXECUTION_TIME_ATTR_NAME, time);
}
log.debug(time);
}
}
在实现的时候,我们不仅仅把执行时间输出到了日志,而且还通过修改ModelAndView对象把这个信息加入到了视图模型内,这样页面也可以展现这个时间。要启用拦截器,我们还需要配置WebConfig:
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new ExecutionTimeHandlerInterceptor());
}
接下去我们运行刚才那个例子,可以看到如下的日志输出:
2018-10-02 19:58:22.189 DEBUG 20422 --- [nio-8080-exec-9] m.j.s.ExecutionTimeHandlerInterceptor : [public java.util.List<me.josephzhu.spring101webmvc.MyItem> me.josephzhu.spring101webmvc.MyRestController.search(me.josephzhu.spring101webmvc.ItemTypeEnum)] executeTime : 22ms
页面上也可以引用到我们添加进去的对象:
<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-4.dtd">
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hello World</title>
</head>
<body>
Hello World
<div th:text="${executionTime}"></div>
</body>
</html>
拦截器是非常通用的一个扩展,可以全局实现权限控制、缓存、动态修改结果等等功能。
总结和讨论Spring MVC
本文我们通过一个一个例子展现了Spring MVC的一些重要扩展点:
1 使用拦截器做执行时间统计
2 自定义ResponseBodyAdvice来处理API的包装
3 自定义ExceptionHandler来统计错误处理
4 自定义ConverterFactory来解析转换枚举
5 自定义ArgumentResolver来组装设备信息参数
6 快速实现静态资源、路径匹配以及ViewController的配置
其实Spring MVC还有很多扩展点,比如模型参数绑定和校验、允许我们实现动态的RequestMapping甚至是DispatcherServlet进行扩展,你可以继续自行研究。
最后,我想说说我对Spring MVC的看法,总体上我觉得Spring MVC实现很灵活,扩展点很多,几乎每一个组件都是松耦合,允许我们自己定义和替换。但是我觉得它的实现有点过于松散。ASP.NET MVC的实现我就挺喜欢,相比Spring MVC,ASP.NET MVC的两个ActionFilter和ActionResult的实现是亮点:
1 ActionFilter机制。Controller里面的每一个方法称作Action,我们可以在每一个Action上加上各种注解来启用ActionFilter,ActionFilter可以针对Action执行前、后、出异常等等情况做回调处理。ASP.NET MVC的ActionFilter的Filer级别是方法,粒度上比拦截器精细很多,而且配置更直观。Spring MVC虽然除了拦截器还有ArgumentResolver以及ReturnValueHandler可以分别进行参数处理和返回值处理,但是这两套扩展体系也是基于框架层面的,如果要和方法打通还需要自定义注解来实现。总觉得Spring MVC的这三套扩展点相互配合功能上虽然完整,但是有种支离破碎的感觉,如果我们真的要实现很多功能的,话可能会在这里有相当多的if-else,没有ActionFilter来得直观。
2 方法的返回值可以是ModelAndView,可以是直接输出到@ResponseBody的自定义类型,这两种输出类型的分法可以满足我们的需求,但是总感觉很别扭。在ASP.NET MVC中的方法返回抽象为了ActionResult,可以是ViewResult、JsonResult、FileContentResult、RedirectResult、FilePathResult、JavaScriptResult等等,正如其名,看到返回值我们就可以看到方法实际的输出表现,非常直观容易理解。
ASP.NET MVC并没有大量依赖IOC和AOP来实现,而是由框架的整体结构实现了插件机制,本质上这和Spring的风格就不同,加上Spring MVC从简化Servlet开始演化,两者理念上的区别也决定了设计上的区别,因此Spring MVC这样设计我也能理解。
朱晔和你聊Spring系列S1E4:灵活但不算好用的Spring MVC的更多相关文章
- Spring系列之IOC的原理及手动实现
目录 Spring系列之IOC的原理及手动实现 Spring系列之DI的原理及手动实现 导语 Spring是一个分层的JavaSE/EE full-stack(一站式) 轻量级开源框架.也是几乎所有J ...
- 朱晔和你聊Spring系列S1E6:容易犯错的Spring AOP
阅读PDF版本 标题有点标题党了,这里说的容易犯错不是Spring AOP的错,是指使用的时候容易犯错.本文会以一些例子来展开讨论AOP的使用以及使用过程中容易出错的点. 几句话说清楚AOP 有关必要 ...
- 朱晔和你聊Spring系列S1E2:SpringBoot并不神秘
朱晔和你聊Spring系列S1E2:SpringBoot并不神秘 [编辑器丢失了所有代码的高亮,建议查看PDF格式文档] 文本我们会一步一步做一个例子来看看SpringBoot的自动配置是如何实现的, ...
- 朱晔和你聊Spring系列S1E1:聊聊Spring家族的几大件
朱晔和你聊Spring系列S1E1:聊聊Spring家族的几大件 [下载本文PDF进行阅读] Spring家族很庞大,从最早先出现的服务于企业级程序开发的Core.安全方面的Security.到后来的 ...
- 朱晔和你聊Spring系列S1E11:小测Spring Cloud Kubernetes @ 阿里云K8S
有关Spring Cloud Kubernates(以下简称SCK)详见https://github.com/spring-cloud/spring-cloud-kubernetes,在本文中我们主要 ...
- 朱晔和你聊Spring系列S1E8:凑活着用的Spring Cloud(含一个实际业务贯穿所有组件的完整例子)
本文会以一个简单而完整的业务来阐述Spring Cloud Finchley.RELEASE版本常用组件的使用.如下图所示,本文会覆盖的组件有: Spring Cloud Netflix Zuul网关 ...
- 朱晔和你聊Spring系列S1E9:聊聊Spring的那些注解
本文我们来梳理一下Spring的那些注解,如下图所示,大概从几方面列出了Spring的一些注解: 如果此图看不清楚也没事,请运行下面的代码输出所有的结果. Spring目前的趋势是使用注解结合Java ...
- Spring系列之手写一个SpringMVC
目录 Spring系列之IOC的原理及手动实现 Spring系列之DI的原理及手动实现 Spring系列之AOP的原理及手动实现 Spring系列之手写注解与配置文件的解析 引言 在前面的几个章节中我 ...
- Spring系列(零) Spring Framework 文档中文翻译
Spring 框架文档(核心篇1和2) Version 5.1.3.RELEASE 最新的, 更新的笔记, 支持的版本和其他主题,独立的发布版本等, 是在Github Wiki 项目维护的. 总览 历 ...
随机推荐
- windows网络编程中文 笔记(二)
IPv4 地址段 IPv4地址类别 种类 网络部分 第1个数字 端点数字 A 8位 0-127 16777216 B 16位 128-191 65526 C 24位 193-223 ...
- C#-委托(十七)
概述 委托(Delegate) 是存有对某个方法的引用的一种引用类型变量 委托特别用于实现事件和回调方法.所有的委托都派生自 System.Delegate 类 委托是一个类,么它就可以被定义在任何地 ...
- OAF--基础
OAF是WEB界面,FORM是由JDK将FORM里面的东西插入到HTML UI里的: OAF由 Oracle Business Components for JAVA(BC4J)框架作为其模型部分,完 ...
- win7 中 sql server2005 卸载简介
注:卸载前一定要做好备份,一定要清理干净,不然重装会出错(只针对完全卸载,没试过只删除一个版本的) 工具:①Windows Install Clean Up ②SrvInstw.exe 1.停止所有 ...
- 转:敏捷开发之Scrum扫盲篇
现在敏捷开发是越来越火了,人人都在谈敏捷,人人都在学习Scrum和XP... 为了不落后他人,于是我也开始学习Scrum,今天主要是对我最近阅读的相关资料,根据自己的理解,用自己的话来讲述Scrum中 ...
- CentOS7 vi编辑命令【转】
CentOS 7 vi编辑命令 用vi打开一个yum文件 vi /usr/bin/yum 按 i 键后 进入insert模式,进入insert模式后才能进行修改 修改完成后 按esc键进入comma ...
- 好系统重装助手教你清理win7系统中DNS缓存
在我们使用电脑的过程中,有时候一个经常用的网页突然打不开了,遇到这种情况,清理一下DNS缓存就可以解决了.如何清理DNS缓存?小编这就给大家说一种最简单的方法. 1.组合键:win+R,输入cmd,点 ...
- JSP(一):初识JSP
在Servlet中,我们多次用到了jsp页面,今天就来仔细聊聊JSP. 一.概念 JSP全名是Java Server Pages,可理解为Java服务端页面,是一种动态网页开发技术,其本质是一个简化的 ...
- 《Java大学教程》--第2章 选择
迭代(iteration).重复(repetition):三种循环* for: 重复执行固定次数* while: 重复执行不固定次数* do...while: 比while至少多一次 1.答:P47迭 ...
- Announcing the Updated NGINX and NGINX Plus Plug‑In for New Relic (Version 2)
In March, 2013 we released the first version of the “nginx web server” plug‑in for New Relic monitor ...