我在后面的文章中介绍了Spring MVC最外围的组件DispatcherServlet,DispatcherServlet把Servlet容器(如Tomcat)中的申请和Spring中的组件分割到一起,是SpringWeb利用的枢纽。然而咱们在日常开发中往往不须要具体晓得枢纽的作用,咱们只须要解决枢纽分发给咱们的申请。Spring中解决申请业务逻辑最常见的组件是Controller,本文会对Spring的Controller及相干组件做具体介绍。
Controller的定义
Controller是Spring中的一个非凡组件,这个组件会被Spring辨认为能够承受并解决网页申请的组件。Spring中提供了基于注解的Controller定义形式:@Controller和@RestController注解。基于注解的Controller定义不须要继承或者实现接口,用户能够自在的定义接口签名。以下为Spring Controller定义的示例。
@Controller
public class HelloController {
@GetMapping("/hello")
public String handle(Model model) {
model.addAttribute("message", "Hello World!");
return "index";
}
}
@Controller注解继承了Spring的@Component注解,会把对应的类申明为Spring对应的Bean,并且能够被Web组件治理。@RestController注解是@Controller和@ResponseBody的组合,@ResponseBody示意函数的返回不须要渲染为View,应该间接作为Response的内容写回客户端。
映射关系RequestMapping
门路的定义
定义好一个Controller之后,咱们须要将不同门路的申请映射到不同的Controller办法之上,Spring同样提供了基于注解的映射形式:@RequestMapping。通常状况下,用户能够在Controller类和办法下面增加@RequestMapping注解,Spring容器会辨认注解并将满足门路条件的申请调配到对应的办法进行解决。在上面的示例中,”GET /persons/xxx”会调用getPerson
办法解决。
@RestController
@RequestMapping("/persons")
class PersonController {
@GetMapping("/{id}")
public Person getPerson(@PathVariable Long id) {
// ...
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public void add(@RequestBody Person person) {
// ...
}
}
门路的匹配
Spring反对两种门路匹配形式,二者之间能够很好的兼容,Spring默认应用PathPattern进行门路的匹配。
- PathPattern:应用预解析的办法匹配门路。专门为Web门路匹配而设计,能够反对简单的表达式,执行效率很高。
- AntPathMatcher:Spring中用于类门路、文件系统和其它资源的解决方案,效率比拟低。
PathPattern根本能够向下兼容AntPathMatcher的逻辑,并且反对门路变量和”**”多段门路匹配,以下列出几种PathPattern的示例:
门路示例 | 阐明 |
---|---|
/resources/ima?e.png | 门路中有一个字符是可变的,如/resources/image.png |
/resources/*.png | 门路中多个字符是可变的,如/resources/test.png |
/resources/** | 门路中多段可变,如/resources/test/path/xxx |
/projects/{project}/versions | 匹配一段门路,并且把门路中的值提取进去,如/projects/MyApp/versions |
/projects/{project:[a-z]+}/versions | 匹配一段合乎正则表达式门路,并且把门路中的值提取进去,如/projects/myapp/versions |
门路中匹配到的变量能够应用@PathVariable获取,Path变量能够是办法或者类级别的,匹配到的变量会主动进行类型转换,如果转换失败则会抛出异样。
@GetMapping("/owners/{ownerId}/pets/{petId}")
public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
// ...
}
@Controller
@RequestMapping("/owners/{ownerId}")
public class OwnerController {
@GetMapping("/pets/{petId}")
public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
// ...
}
}
门路抵触
当一次申请匹配到多个Pattern,那么就须要选出最靠近的Pattern门路。Spring为Pattern和AntPathMatcher提供了抉择最靠近的门路策略,二者之间逻辑相近,此处只介绍PathPattern。对于PathPattern,Spring提供了PathPattern.SPECIFICITY_COMPARATOR用于比照门路之间的优先级,比照的规定如下:
- null的pattern具备最低优先级。
- 蕴含通配符的pattern的具备最低优先级(如/**)。
- 如果两个pattern都蕴含通配符,长度比拟长的有更高的优先级。
- 蕴含越少匹配符号和越少门路变量的pattern有越高的优先级。
- 门路越长的优先级越高。
Spring 5.3之后不再反对.*后缀匹配,默认状况下“/person”就会匹配到所有的 “/person.*”
承受和返回参数的类型
RequestMapping还能够指定接口承受什么类型的参数以及返回什么类型的参数,这通常会在申请头的Content-Type中指定:
@PostMapping(path = "/pets", consumes = "application/json")
public void addPet(@RequestBody Pet pet) {
// ...
}
@GetMapping(path = "/pets/{petId}", produces = "application/json")
@ResponseBody
public Pet getPet(@PathVariable String petId) {
// ...
}
依据参数或Header抉择
RequestMapping还反对依照申请的参数或者Header判断是否解决申请。
-
如只承受参数myParam的值为myValue的状况,能够通过如下形式指定:
@GetMapping(path = "/pets/{petId}", params = "myParam=myValue") public void findPet(@PathVariable String petId) { // ... }
-
如只承受申请头中myParam的值为myValue的状况,能够通过如下形式指定:
@GetMapping(path = "/pets", headers = "myHeader=myValue") public void findPet(@PathVariable String petId) { // ... }
编程式注册RequestMapping
咱们后面的教程中讲的都是怎么通过@RequestMapping进行门路的映射,应用这种形式会主动把门路映射为增加了注解的办法。这种形式尽管应用很不便,然而灵活性方面有一些欠缺,如果我想要依据Bean的配置信息动静映射门路之间的关系时,注解的形式就无奈做到这种需要。Spring提供了一种动静注册RequestMapping的办法,注册示例如下所示:
@Configuration
public class MyConfig {
// 从容器中获取保护映射关系的RequestMappingHandlerMapping和自定义组件UserHandler
@Autowired
public void setHandlerMapping(RequestMappingHandlerMapping mapping, UserHandler handler)
throws NoSuchMethodException {
// 生成门路匹配信息
RequestMappingInfo info = RequestMappingInfo
.paths("/user/{id}").methods(RequestMethod.GET).build();
// 获取须要映射的办法
Method method = UserHandler.class.getMethod("getUser", Long.class);
// 注册门路和办法之间的映射信息
mapping.registerMapping(info, handler, method);
}
}
解决办法
通过RequestMapping映射通常能够把顺次申请映射到某个办法,这个办法就是解决办法(Handler Methods)。解决办法的参数和返回值能够应用很多申请中的信息(如@RequestParam, @RequestHeader)等,这些参数反对应用Optional进行封装。
办法参数 | 阐明 |
---|---|
WebRequest, NativeWebRequest | 蕴含了申请参数、申请和Session信息,次要用于Spring框架外部解析参数等操作 |
javax.servlet.ServletRequest, javax.servlet.ServletResponse | Servlet的申请和参数信息 |
javax.servlet.http.HttpSession | 申请的Session信息 |
javax.servlet.http.PushBuilder | 服务器推送是HTTP/2协定中的新个性之一,旨在通过将服务器端的资源推送到浏览器的缓存中来预测客户端的资源需要,以便当客户端发送网页申请并接管来自服务器的响应时,它须要的资源曾经在缓存中。这是一项进步网页加载速度的性能加强的性能。在Servlet 4.0中,服务器推送性能是通过PushBuilder实例公开的,此实例是从HttpServletRequest实例中获取的。 |
java.security.Principal | 以后用户的登录信息 |
HttpMethod | 申请的形式,如GET,POST等 |
java.util.Locale | 申请中的国际化信息 |
java.util.TimeZone + java.time.ZoneId | 申请的时区信息 |
java.io.InputStream, java.io.Reader | 用于获取申请原始Body的输出流 |
java.io.OutputStream, java.io.Writer | 用于写回响应的输入流 |
@PathVariable | 门路变量,如”/pets/{petId}”中的petId |
@MatrixVariable | 用分号宰割的参数,如GET /pets/42;q=11;r=22 |
@RequestParam | 获取申请中的参数,蕴含multipart类型的文件 |
@RequestHeader | 申请头信息 |
@CookieValue | 申请中的Cookie信息 |
@RequestBody | 把申请的Body,会应用HttpMessageConverter转为指定的类型的数据。 |
HttpEntity\<B> | 相似于@RequestBody |
@RequestPart | 用于获取multipart/form-data中的数据 |
java.util.Map, org.springframework.ui.Model, org.springframework.ui.ModelMap | 获取用于渲染HTML视图的参数 |
@ModelAttribute | 用于获取模型中的属性 |
Errors, BindingResult | 获取参数校验后果信息 |
SessionStatus + class-level @SessionAttributes | Session信息 |
UriComponentsBuilder | 获取匹配过程中的参数信息 |
@SessionAttribute | 获取一个Session属性 |
@RequestAttribute | 获取申请中的属性 |
解决办法也能够反对很多类型的返回值,不同类型的返回有不同的意义。
返回参数 | 阐明 |
---|---|
@ResponseBody | @RestController就蕴含了这个注解,这个注解示意应用HttpMessageConverter把返回值写入Response,不会进行视图解析 |
HttpEntity\<B>, ResponseEntity\<B> | 和@ResponseBody相似,返回值间接写入Response |
HttpHeaders | 只返回Header不返回body |
String | 依照返回值去查找View,并解析为模型 |
View | 返回一个视图 |
java.util.Map, org.springframework.ui.Model | 用于渲染视图的模型,View由RequestToViewNameTranslator决定 |
@ModelAttribute | 用于渲染视图的模型,View由RequestToViewNameTranslator决定 |
ModelAndView | 返回一个可用的模型视图 |
void | 通常示意没有返回Body |
DeferredResult\<V> | 异步返回后果,后文具体介绍 |
Callable\<V> | 异步返回后果,后文具体介绍 |
ListenableFuture\<V>, java.util.concurrent.CompletionStage\<V>, java.util.concurrent.CompletableFuture\<V> | 相似于DeferredResult,异步返回调用后果 |
ResponseBodyEmitter, SseEmitter | 异步的把HttpMessageConverter转换后的Body写入Response |
StreamingResponseBody | 把返回异步写入Response |
Reactive types — Reactor, RxJava, or others through ReactiveAdapterRegistry | Flux场景下的异步返回 |
类型转换
网络申请的参数往往是String类型的,而映射到后端时须要转为解决办法须要的数据类型(如@RequestParam, @RequestHeader, @PathVariable, @MatrixVariable 和 @CookieValue)。这种状况下Spring会获取容器内的类型转换服务和属性编辑器进行转换,用户也能够向WebDataBinder中注入本人须要的转换服务。
Matrix参数
Matrix参数其实时RFC3986中对于Url编码的一些标准,Matrix参数之间用分号宰割,Matrix参数的多个值之间用逗号宰割,例如/cars;color=red,green;year=2012
,多个值之间也容许用分号宰割,如color=red;color=green;color=blue
如果一个URL须要蕴含Matrix参数,那么蕴含Matrix参数应该是一个门路变量,否则Matrix参数会对门路匹配造成影响:
// GET /pets/42;q=11;r=22
// 最初一段门路必须为门路变量{petId},否则会造成门路匹配失败
@GetMapping("/pets/{petId}")
public void findPet(@PathVariable String petId, @MatrixVariable int q) {
// petId == 42
// q == 11
}
不仅仅URL最初一段能够加Matrix参数,URL的任意一段都能够家Matrix参数,如下所示:
// GET /owners/42;q=11/pets/21;q=22
@GetMapping("/owners/{ownerId}/pets/{petId}")
public void findPet(
@MatrixVariable(name="q", pathVar="ownerId") int q1,
@MatrixVariable(name="q", pathVar="petId") int q2) {
// q1 == 11
// q2 == 22
}
Matrix参数容许设置默认值,用户没有传该参数的时候应用这个默认值:
// GET /pets/42
@GetMapping("/pets/{petId}")
public void findPet(@MatrixVariable(required=false, defaultValue="1") int q) {
// q == 1
}
如果门路中蕴含很多Matrix参数,一个一个接管可能比拟麻烦,咱们能够通过MultiValueMap用汇合的模式去接管:
// GET /owners/42;q=11;r=12/pets/21;q=22;s=23
@GetMapping("/owners/{ownerId}/pets/{petId}")
public void findPet(
@MatrixVariable MultiValueMap<String, String> matrixVars,
@MatrixVariable(pathVar="petId") MultiValueMap<String, String> petMatrixVars) {
// matrixVars: ["q" : [11,22], "r" : 12, "s" : 23]
// petMatrixVars: ["q" : 22, "s" : 23]
}
如果你须要在程序中应用Matrix参数,须要的配置
UrlPathHelper
的removeSemicolonContent=false
。
@RequestParam
@RequestParam用于把申请中的参数(查问参数或者表单参数)绑定到对应的办法参数上,默认状况下不容许申请参数中不蕴含指定的参数,不过用户能够指定required=false
去容许设置申请参数到对应的办法参数。如果办法的参数类型不为String类型,Spring会主动进行类型转换。当@RequestParam注解的参数类型为Map\<String, String>并且@RequestParam没有指定参数名称的时候,Spring会把所有的参数注入到Map中。
@Controller
@RequestMapping("/pets")
public class EditPetForm {
// ...
@GetMapping
public String setupForm(@RequestParam("petId") int petId, Model model) {
Pet pet = this.clinic.loadPet(petId);
model.addAttribute("pet", pet);
return "petForm";
}
// ...
}
@RequestHeader
一次Http申请往往会蕴含申请头和Body两局部,咱们能够通过@RequestHeader把申请头和解决办法的参数进行绑定,@RequestHeader同样反对Map,假如一次申请有如下的头:
Host localhost:8080
Accept text/html,application/xhtml+xml,application/xml;q=0.9
Accept-Language fr,en-gb;q=0.7,en;q=0.3
Accept-Encoding gzip,deflate
Accept-Charset ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive 300
如果咱们须要在办法中获取Accept-Encoding和Keep-Alive标签,咱们能够通过如下代码获取:
@GetMapping("/demo")
public void handle(
@RequestHeader("Accept-Encoding") String encoding,
@RequestHeader("Keep-Alive") long keepAlive) {
//...
}
@CookieValue
如果咱们须要获取一次申请中的cookie信息,咱们能够通过@CookieValue获取,获取办法如下所示:
@GetMapping("/demo")
public void handle(@CookieValue("JSESSIONID") String cookie) {
//...
}
@ModelAttribute
@ModelAttribute能够把申请中的参数映射为对象,而后传递给对应的办法。
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute Pet pet) {
// method logic...
}
下面的例子中,申请参数能够来自pet
能够来自以下几种路径:
- 在申请预处理的过程中增加的@ModelAttribute属性中的pet;
- 从HttpSession中的@SessionAttributes属性中查找pet;
- 从申请参数或者pathVariable中查找pet属性;
- 应用默认的构造函数初始化数据。
@PutMapping("/accounts/{account}")
public String save(@ModelAttribute("account") Account account) {
// ...
}
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) {
if (result.hasErrors()) {
return "petForm";
}
// ...
}
@ModelAttribute
public AccountForm setUpForm() {
return new AccountForm();
}
@ModelAttribute
public Account findAccount(@PathVariable String accountId) {
return accountRepository.findOne(accountId);
}
@PostMapping("update")
public String update(@Valid AccountForm form, BindingResult result,
@ModelAttribute(binding=false) Account account) {
// ...
}
@SessionAttributes 和 @SessionAttribute
@SessionAttributes用于在多个申请之间共享Session数据,该注解只能加载类之上。在第一次申请的时候,会把Session数据放入SessionAttributes中,Session完结的时候革除数据。
@Controller
@SessionAttributes("pet") // 把数据放入Session中
public class EditPetForm {
// 从Session中查问数据
@PostMapping("/pets/{id}")
public String handle(Pet pet, BindingResult errors, SessionStatus status) {
if (errors.hasErrors) {
// ...
}
// 清空Session中的数据.
status.setComplete();
// ...
}
}
}
如果Session的属性不禁Controller治理,而是其它组件治理(如Filter治理),咱们就能够应用@SessionAttribute去把Session中的数据和解决办法中的参数进行绑定。
@RequestMapping("/")
public String handle(@SessionAttribute User user) {
// ...
}
@RequestAttribute
@RequestAttribute和@SessionAttributes相似,一个是申请级别的,一个是Session级别的,此处不做具体介绍。
@GetMapping("/")
public String handle(@RequestAttribute Client client) {
// ...
}
Multipart参数
咱们在后面的文章中说过,DispatcherServlet中会蕴含MultipartResolver组件,如果一次申请的数据为multipart/form-data
类型,DispatcherServlet会把上传的文件解析为MultipartFile格局的文件。Servlet3中也反对应用 javax.servlet.http.Part代替MultipartFile接管文件,上传多个文件的时候能够应用列表或者Map获取参数。
@Controller
public class FileUploadController {
@PostMapping("/form")
public String handleFormUpload(@RequestParam("name") String name,
@RequestParam("file") MultipartFile file) {
if (!file.isEmpty()) {
byte[] bytes = file.getBytes();
// store the bytes somewhere
return "redirect:uploadSuccess";
}
return "redirect:uploadFailure";
}
}
Multipart也能够把须要接管的文件封装为对象进行接管。
class MyForm {
private String name;
private MultipartFile file;
// ...
}
@Controller
public class FileUploadController {
@PostMapping("/form")
public String handleFormUpload(MyForm form, BindingResult errors) {
if (!form.getFile().isEmpty()) {
byte[] bytes = form.getFile().getBytes();
// store the bytes somewhere
return "redirect:uploadSuccess";
}
return "redirect:uploadFailure";
}
}
除了通过浏览器上传文件,咱们还能够通过RestFul形式以Json的格局上传文件:
POST /someUrl
Content-Type: multipart/mixed
--edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp
Content-Disposition: form-data; name="meta-data"
Content-Type: application/json; charset=UTF-8
Content-Transfer-Encoding: 8bit
{
"name": "value"
}
--edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp
Content-Disposition: form-data; name="file-data"; filename="file.properties"
Content-Type: text/xml
Content-Transfer-Encoding: 8bit
... File Data ...
@PostMapping("/")
public String handle(@RequestPart("meta-data") MetaData metadata,
@RequestPart("file-data") MultipartFile file) {
// ...
}
@RequestBody和HttpEntity
@RequestBody应该是日常开发中应用最多的参数之一了,咱们能够通过@RequestBody把申请中的Body和解决办法中的参数对象进行绑定,Spring会调用HttpMessageConverter服务把申请中的数据反序列化为解决办法中的参数对象。@RequestBody还能够和@Validated注解组合进行应用,如果校验失败会抛出异样或者交给用户解决校验异样信息。
@PostMapping("/accounts")
public void handle(@Valid @RequestBody Account account, BindingResult result) {
// ...
}
HttpEntity和@RequestBody的原理相似,不过会把申请体封装到HttpEntity中。
@PostMapping("/accounts")
public void handle(HttpEntity<Account> entity) {
// ...
}
@ResponseBody和ResponseEntity
@ResponseBody示意会把返回值通过HttpMessageConverter间接序列化为String写入Response,咱们平时应用比拟多的@RestController就是由@ResponseBody和@Controller组成。
@GetMapping("/accounts/{id}")
@ResponseBody
public Account handle() {
// ...
}
ResponseEntity和@ResponseBody,不过返回的根底上会蕴含状态码和返回头等信息。
@GetMapping("/something")
public ResponseEntity<String> handle() {
String body = ... ;
String etag = ... ;
return ResponseEntity.ok().eTag(etag).build(body);
}
JSON Views
Spring内置了对JacksonJSON的反对,并且反对Jackson的Json序列化视图,在应用@ResponseBody和ResponseEntity返会数据时,能够依照@JsonView来指定Json序列化时须要显示的字段。
@RestController
public class UserController {
@GetMapping("/user")
@JsonView(User.WithoutPasswordView.class)
public User getUser() {
return new User("eric", "7!jd#h23");
}
}
public class User {
public interface WithoutPasswordView {};
public interface WithPasswordView extends WithoutPasswordView {};
private String username;
private String password;
public User() {
}
public User(String username, String password) {
this.username = username;
this.password = password;
}
@JsonView(WithoutPasswordView.class)
public String getUsername() {
return this.username;
}
@JsonView(WithPasswordView.class)
public String getPassword() {
return this.password;
}
}
咱们也能够通过编程的形式实现对象的不同视图的序列化,应用办法如下所示:
@RestController
public class UserController {
@GetMapping("/user")
public MappingJacksonValue getUser() {
User user = new User("eric", "7!jd#h23");
MappingJacksonValue value = new MappingJacksonValue(user);
value.setSerializationView(User.WithoutPasswordView.class);
return value;
}
}
对于基于View的解决方案,咱们能够在Model中增加对应的对象以及Json序列化视图,应用的示例如下所示:
@Controller
public class UserController extends AbstractController {
@GetMapping("/user")
public String getUser(Model model) {
model.addAttribute("user", new User("eric", "7!jd#h23"));
model.addAttribute(JsonView.class.getName(), User.WithoutPasswordView.class);
return "userView";
}
}
Model对象
Spring中的model对象负责在控制器和展示数据的视图之间传递数据。Spring提供了@ModelAttribute去获取和写入Model对象的属性,@ModelAttribute有多种应用形式:
- 在解决办法的入参上增加@ModelAttribute,能够获取WebDataBinder中曾经有的Model中的属性值。
- 在类上(如Controller)增加@ModelAttribute注解,则会为所有的申请初始化模型。
- 在解决办法的返回值上增加@ModelAttribute,示意返回值会作为模型的属性。
@ModelAttribute
public void populateModel(@RequestParam String number, Model model) {
model.addAttribute(accountRepository.findAccount(number));
// add more ...
}
@ModelAttribute
public Account addAccount(@RequestParam String number) {
return accountRepository.findAccount(number);
}
DataBinder
后面咱们讲了很多如何把申请参数和解决办法入参进行绑定的注解或者类型,并且晓得申请参数须要通过类型转换能力转为对应类型的数据。然而注解只是一个标记,并不会理论执行参数绑定和类型转换操作,Spring中必然有一个组件进行参数绑定和类型转换,这个组件就是WebDataBinder。WebDataBinder有一下作用:
- 将申请中的参数和解决办法参数进行绑定;
- 把申请中Spring类型的数据转为解决办法的参数类型;
- 对渲染表单的数据进行格式化。
Spring给用户提供了批改WebDataBinder的接口,用户能够在Controller中定义被@InitBinder注解的办法,在办法中批改WebDataBinder的定义:
@Controller
public class FormController {
@InitBinder
public void initBinder(WebDataBinder binder) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
dateFormat.setLenient(false);
binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
}
// ...
}
异样解决
在对于DispatcherServlet相干的章节中,咱们晓得了DispatcherServlet蕴含了异样解析组件,当异样产生的时候会对异样进行解析。日常开发中应用比拟多的异样解决组件是ExceptionHandlerExceptionResolver,用于在遇到异样时,应用带有@ExceptionHandler注解的办法解决对应的异样,该办法能够定义中Controller或者ControllerAdvice中。
@Controller
public class SimpleController {
// ...
@ExceptionHandler
public ResponseEntity<String> handle(IOException ex) {
// ...
}
@ExceptionHandler({FileSystemException.class, RemoteException.class})
public ResponseEntity<String> handle(Exception ex) {
// ...
}
}
如果咱们须要定义很多@ExceptionHandler,咱们能够抉择在@ControllerAdvice中定义,而不是在每个Controller中定义。
如果一个异样匹配到多个@ExceptionHandler,Spring会尝试应用间隔异样继承体系最近的@ExceptionHandler去解决这个异样。
Controller Advice
如果咱们须要定义全局的@InitBinder或者@ExceptionHandler,那咱们就不应该在Controller中定义这些办法。 Spring提供了@ControllerAdvice用于增加全局配置:
// Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}
// Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}
// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class ExampleAdvice3 {}
我是御狐神,欢送大家关注我的微信公众号:wzm2zsd
本文最先公布至微信公众号,版权所有,禁止转载!
发表回复