[spring注解]Spring相关注解(二)

前言

本篇文章,我们接着来学习spring-web对应jar包中的注解,Spring版本:4.3.14。本篇要学习的注解包括:MatrixVariableSessionAttributeSessionAttributesResponseStatusRequestAttributeRequestHeaderCookieValueRequestPartCrossOrigin

一、Spring-web相关注解

1. MatrixVariable注解

applicationContext.xml-mvc标签 这篇文章中,我们已经了解过什么是Matrix-variable,其实说白了就是URL中使用键值对的这种特性。matrix变量可以出现在任何路径中,使用分号进行分割,而对应的该注解用于参数上来绑定URL中的matrix变量,比如:"/cars;color=red;year=2012",而多个值是使用逗号进行分割,比如:"/color=red,green,blue",或者也可以使用重复的变量名,如:color=red;color=green;color=blue
首先来简单看下它的几个属性:

  1. name,这个很简单,matrix变量的名称;
  2. value,和name功能一样,是name的别名;
  3. pathVar,如果需要进行消除歧义(例如在多个路径中存在相同名称的matrix变量),可以设置该URI路径变量的名称;
  4. required,是否必须,默认是true;
  5. defaultValue,默认值,不多说了;

不过默认情况下Spring MVC是没有开启该功能的,需要我们去配置下,如果我们使用XML的话,那么对应的配置是enable-matrix-variables,该参数默认配置为false:

<mvc:annotation-driven enable-matrix-variables="true" />

而如果我们使用纯注解开发的话,必须设置 RequestMappingHandlerMapping 中的 removeSemicolonContent 为 false,默认情况下该设置是true。由于我的配置类SpringConfig是继承自WebMvcConfigurerAdapter,所以只需要覆盖它的方法configurePathMatch即可,这里RequestMappingHandlerMapping会委托给UrlPathHelper去查找相应的path,我们可以按照如下配置:

@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
    UrlPathHelper urlPathHelper = new UrlPathHelper();
    urlPathHelper.setRemoveSemicolonContent(false);
    configurer.setUrlPathHelper(urlPathHelper);
}

接下来我们来看下官网提供的几个例子:

// GET /pets/42;q=11;r=22
@GetMapping("/pets/{petId}")
public void findPet(@PathVariable String petId, @MatrixVariable int q) {
    // petId == 42
    // q == 11
}

根据传入的参数/pets/42;q=11;r=22,我们可以得到对应的q的值是11。

在复杂的情况下,由于路径的每部分都可能包含matrix变量,在某些情况下,您需要更具体地确定变量的位置,这时候就可以借助 pathVar 变量:

// 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
    System.out.println(q1);
    System.out.println(q2);
}

我们可以通过pathVar参数,也就是占位符的名称 来区分URL参数中两个q变量的值。

同样,matrix变量也可以定义为可选的,也可以指定默认值:

// GET /pets/42
@GetMapping("/pets/{petId}")
public void findPet(@MatrixVariable(required=false, defaultValue="1") int q) {
    // q == 1
    System.out.println(q);
}

当然,matrix变量的接收参数也可以是Map,如果是Map并且没有指定matrix的变量名,那么该map将包含所有的matrix变量和对应的值:

// 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" : 11, "s" : 23]
    System.out.println(matrixVars);
    System.out.println(petMatrixVars);
}

这里简单说下,MultiValueMap是Spring提供的一个接口,功能是一个key可以对应多个value,从上面的例子可以看出,属性matrixVars的一个key是q,对应的值有多个。我们可以看下MultiValueMap的继承结构就可以看出来了:

public interface MultiValueMap<K, V> extends Map<K, List<V>> {
}

因为一般情况下,Map中的key是不允许重复的,我们将上面例子中的MultiValueMap修改为Map,然后传入相同的参数(有两个q参数),来看一下结果:

// GET /owners/42;q=11;r=12/pets/21;q=22;s=23
@RequestMapping(value = "/owners/{ownerId}/pets/{petId}", method = RequestMethod.GET)
public void findPet(@MatrixVariable Map<String, String> matrixVars,
                    @MatrixVariable(pathVar = "ownerId") Map<String, String> petMatrixVars) {
    System.out.println(matrixVars);
    System.out.println(petMatrixVars);
}

看一下打印结果:

{q=11, r=12, s=23}
{q=11, r=12}

可以看出map对象matrixVars中的q的值保存的是第一个,这里可能需要我们注意,可以使用pathVar来区分,从而避免出现这种情况。

如需有关matrix-variable的更多内容,可以查看官网文档:https://docs.spring.io/spring/docs/4.3.14.RELEASE/spring-framework-reference/html/mvc.html
有关matrix的使用可参考这篇文章:https://blog.csdn.net/csdnfanguyinheng/article/details/51586633

2. SessionAttribute,SessionAttributes注解
SessionAttributes

  默认情况下,ModelMap中属性的作用域是request级别,也就是说,当本次请求结束后,Model中的属性将被销毁,如果希望在多个请求中共享 ModelMap 中的属性,必须将其属性转存到 session 中,这样 ModelMap 的属性才可以被跨请求访问。
  Spring 允许我们有选择地指定 ModelMap 中的哪些属性需要转存到 session 中,以便下一个请求能在 ModelMap 的属性列表中访问到这些属性。而这一功能的实现则是通过@SessionAttributes和SessionAttribute注解来实现的。

先来看下SessionAttributes注解的几个参数:

  1. namesvalue,存储于session中的Model属性的名称,数组格式;
  2. types,存储于session中的Model属性的类型,数组格式;

接下来,通过简单的例子来学习一下它们的用法。

@RestController
@RequestMapping("/world")
@SessionAttributes("currUser")
public class WorldController {

    @RequestMapping(value = "/test.html", method = RequestMethod.GET)
    public String test(User user, ModelMap modelMap) {
        System.out.println(user);
        // 添加属性名为currUser的对象
        modelMap.addAttribute("currUser", user);
        return "test";
    }
}

我们将ModelMap中属性名为currUser的属性放到Session属性列表中,以便这个属性可以跨请求访问。然后我们在另一个控制器方法中查看session中对应的该属性。

@RequestMapping(value = "/test2.html", method = RequestMethod.GET)
public String test2(HttpServletRequest request, HttpSession session) {
    System.out.println("currUser:" + session.getAttribute("currUser"));
    return "test2";
}

首先,我们发送第一个方法对应的请求:http://localhost:8080/world/test.html?id=123&name=zhangsan,然后再发送第二个方法对应的请求:http://localhost:8080/world/test2.html,然后看一下打印出的对象:

User{name='zhangsan', id='123', aDouble=null}
currUser:User{name='zhangsan', id='123', aDouble=null}

可以看到session中已经保存了我们的对象。而如果要清除session中的对象,可以使用
结合文档和上面的例子,我们可以看到:

    1. 这里我们仅将一个 ModelMap 的属性放入 Session 中,其实 @SessionAttributes 允许指定多个属性。你可以通过字符串数组的方式指定多个属性,如 @SessionAttributes({“attr1”,”attr2”});
    1. 此外,@SessionAttributes 还可以通过属性类型指定要存到 session 的 ModelMap 属性,如 @SessionAttributes(types = User.class),当然也可以指定多个类,如 @SessionAttributes(types = {User.class,Dept.class}),还可以联合使用属性名和属性类型指定:@SessionAttributes(types = {User.class,Dept.class},value={“attr1”,”attr2”})
    1. 我们还可以配合@ModelAttribute注解来实现将传入的参数自动添加到session中;
    1. 而如果要清除session中的对象,可以通过SessionStatus的setComplete()方法来完成,但它只清除的是@SessionAttributes的session,不会清除HttpSession的数据;
    1. 根据官方文档,对于永久会话属性(如:用户身份验证对象),请使用传统的session.setAttribute方法,另外,也可考虑使用WebRequest来实现。
SessionAttribute

接下来再说下SessionAttribute这个注解。根据文档,我们知道,这个注解用于将全局session中已存在的属性绑定到参数上,和@SessionAttributes定义在类上不同,@SessionAttribute是定义在方法上的,并且需要注意下:

@SessionAttribute是Spring4.3之后引入的;

If you need access to pre-existing session attributes that are managed globally, i.e. outside the controller (e.g. by a filter), and may or may not be present use the @SessionAttribute annotation on a method parameter:

也就是说,通过该注解,我们可以将session(也就是javax.servlet.http.HttpSession)中已经存在的属性绑定到对应的参数上,不过要注意,是已经存在的属性。与其直接使用HttpSession,使用该注解还可以做一些类型的自动转换和可选/必需的校验,下面我们来简单看一下参数:

  1. namevalue,session中存在的属性的名称,默认是根据方法中参数的名称进行推断出来的;
  2. required,session中是否存在该属性,默认是true。如果属性不在session中,那么Spring将会抛出异常,不过在设置为false的情况下,可以接收值为null或者是 Java 8 中的 java.util.Optional。

该注解可用于在全局范围内管理已存在的会话属性,例如在控制器中,例如在过滤器、拦截器等中,适用于存储/访问用户身份验证信息等相关操作。

我们来看一个简单的例子:
我们先在session中存储一个user属性:

@RestController
@RequestMapping("/world")
@SessionAttributes("user")
public class WorldController {

    @RequestMapping(value = "/test2.html", method = RequestMethod.GET)
    public String test2(HttpServletRequest request, HttpSession session) {
        System.out.println("test");
        System.out.println(session.getAttribute("user"));
        return "test2";
    }

    @ModelAttribute("user")
    public User getUser() {
        User user = new User();
        user.setId("id");
        user.setName("name");
        return user;
    }

}

然后,再另一个控制器中使用@SessionAttribute注解来获取session中已经存在的user对象:

@RestController
public class HelloController {
    @RequestMapping(value = "/test2.html", method = RequestMethod.GET)
    public String test2(@SessionAttribute(name = "user", required = false) User user) {
        System.out.println(user);
        return "test2";
    }
}

我们先发送请求,用于向session中保存user对象:/world/test2.html,再发送请求:test2.html,来获取session,并绑定到对象user上,然后查看下打印结果:

User{name='name', id='id', aDouble=null}

可以看到,数据已经成功绑定到我们所需要的对象上了。当然这时候如果从客户端传参数绑定到对象上的话,也是绑不上去的。

这里参考了如下文章:
Spring MVC - Accessing pre-existing session attributes using @SessionAttribute
spring学习之@SessionAttributes
https://docs.spring.io/spring/docs/4.3.14.RELEASE/spring-framework-reference/html/mvc.html

3. ResponseStatus 注解

  ResponseStatus注解是用于标记一个方法或异常类在返回时响应的http状态,用于方法和异常类上,也可以再Controller上使用此注解,这样所有的RequestMapping都会继承,还可以结合ResponseBody 注解一起使用。

大致了解下它的参数:

  1. code,请求响应的状态码,默认是HttpStatus.INTERNAL_SERVER_ERROR,也就是500;
  2. value,code 别名;
  3. reason,对应的错误信息;

我们来看一个简单的例子,先定义我们的异常类:

@ResponseStatus(value = HttpStatus.FORBIDDEN, reason = "用户相关异常")
public class UserException extends RuntimeException {
}

然后在Controller中抛出该异常:

@RestController
public class HelloController {
    @RequestMapping(value = "/test2.html", method = RequestMethod.GET)
    public String test2(User user) {
        if (user.getId() == null) {
            throw new UserException();
        }
        return "test2";
    }
}

最终结果:


异常截图.png

我们再来看下该注解修饰方法的情况:

@RestController
public class HelloController {
   @ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "找不到用户信息喽")
   @RequestMapping(value = "/test2.html", method = RequestMethod.GET)
    public String test2(User user) {
        if (user.getId() == null) {
            throw new UserException();
        }
        return "test2";
    }
}

然后,我们访问该地址,先不传参数id,然后错误页面和上面一样,因为进了UserException异常。然后我们传id参数,然后可以看到:


异常截图.png

但方法的整体流程还是执行完成了,也就是说,在方法上使用该注解,并不影响程序的流程,只是在最终返回的时候会根据状态码和错误信息显示错误页面。

4. RequestAttribute注解

  Spring4.3之后添加的注解,用于方法参数上,用于将Web请求(request attribute)中的属性绑定到方法参数上,也可以用于将由过滤器或拦截器创建的属性绑定到方法参数上。和@SessionAttribute类似,也是用于绑定已经存在的属性。
  参数简单说下,valuename是要绑定的request attribute的名称,required表示该请求属性是否必需,默认是true,这种情况下如果该请求属性不存在的话将会抛出异常。

简单看下例子:

@RestController
public class HelloController {
    @RequestMapping(value = "/test1.html", method = RequestMethod.GET)
    public String test2(@RequestAttribute User user) {
        System.out.println(user);
        return "test1";
    }

    @ModelAttribute
    public void user(HttpServletRequest request) {
        User user = new User();
        user.setName("name");
        user.setId("id");
        request.setAttribute("user", user);
    }
}

访问下该接口:/test1.html,查看下我们获取到的数据:

User{name='name', id='id', aDouble=null}
5. RequestHeader注解

  RequestHeader注解用于方法参数,目的是为了将Http的请求头绑定到方法参数上,和HttpServletRequest.getHeader(String)是一样的。该注解的属性很简单,就不看了,直接来看一个简单的例子即可:

@RequestMapping(value = "/test1.html", method = RequestMethod.GET)
public String test2(@RequestHeader("Accept-Language") String language) {
    System.out.println(language);
    return "test1";
}

打印结果:

zh-CN,zh;q=0.9

如果有兴趣的话,可以了解下Http请求头各个属性的具体含义,我们也可以在发送请求的时候添加一些请求头,然后在后台接收一下。

6. CookieValue注解

  CookieValue注解也是用于方法参数,目的是为了将Http的某个cookie值绑定到方法参数上,而要绑定的方法参数类型可以是Cookie对象,字符串,整数类型等;而该注解的几个属性和RequestHeader一样,不多说了,也是来看个简单的例子:

@RequestMapping(value = "/test1.html", method = RequestMethod.GET)
public String test2(@CookieValue("JSESSIONID") String jessionId) {
    System.out.println(jessionId);
    return "test1";
}

最终会打印出我们cookie中的JSESSIONID值:

97F942850C6E27905758DCA4E1608CF0
7. RequestPart注解

  RequestPart注解也是用于方法参数,目的是为了将文件之类的multipart,也就是multipart/form-data表单提交的绑定到方法参数上。
  而RequestParams也是完全支持这种类型的,它和RequestPart主要的区别,就是请求方法的参数类型不是字符串类型的时候,RequestParam通过注册的转换器或PropertyEditor进行类型转换,而RequestPart则依赖于HttpMessageConverters和请求中的Content-Type。@RequestParam适用于name-value类型的请求域,@RequestPart适用于复杂的请求域(像JSON,XML)等。

Note that @RequestParam annotation can also be used to associate the part of a "multipart/form-data" request with a method argument supporting the same method argument types. The main difference is that when the method argument is not a String, @RequestParam relies on type conversion via a registered Converter or PropertyEditor while @RequestPart relies on HttpMessageConverters taking into consideration the 'Content-Type' header of the request part. @RequestParam is likely to be used with name-value form fields while @RequestPart is likely to be used with parts containing more complex content (e.g. JSON, XML).

该注解的参数很简单,就不多说了,下面来简单看下,先看下RequestParam:

@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";
    }
}

再来看下RequestPart:

@Controller
public class FileUploadController {
    @PostMapping("/form")
    public String handleFormUpload(@RequestParam("name") String name,
            @RequestParam("file") Part file) {
        InputStream inputStream = file.getInputStream();
        // store bytes from uploaded file somewhere
        
        return "redirect:uploadSuccess";
    }
}

代码摘自官方文档,地址:https://docs.spring.io/spring/docs/4.3.14.RELEASE/spring-framework-reference/html/mvc.html#mvc-multipart

其实,RequestPart和RequestParams在multipart/form-data方面的功能很相似,但RequestParams一般用于简单的name-value这种形式的参数绑定,而RequestPart则更多的是用于比较复杂的类型,比如JSON,XML等。

8. CrossOrigin注解

  CrossOrigin注解是用于处理跨域请求的注解,用于类级别和方法级别上。跨域,通俗点说,就是浏览器不能在当前网站执行其他网站的脚本,这是由于浏览器的同源策略造成的,是浏览器对JS做的安全限制。所谓 同源,是指域名,协议,端口均相同,只要有一个不同就算是跨域。

该注解是Spring4.2之后引入的。

下面我们来看一下它的一些参数(已过时的就不说了):

    1. originsvalue,允许跨域访问的来源列表,数组格式,比如http://domain1.com,或者* 允许所有来源,默认情况下,所有请求的域都支持。而这些值将都展示在请求头中的Access-Control-Allow-Origin中,value则是 origins的别名。
    1. allowedHeaders,在请求的时候允许的请求头列表,数组格式,*表示允许所有请求头,默认情况下,所有请求头都支持。同样,这些值都将展示在请求头Access-Control-Allow-Headers中,如果请求头名称是以下列表中的其中之一,则不必列出来:Cache-Control, Content-Language, Expires, Last-Modified, or Pragma as per the CORS spec。
    1. exposedHeaders,响应头中允许的headler,即:Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, or Pragma等,默认情况下是空,同样,这些值都将展示在Access-Control-Expose-Headers列表中;
    1. methods,支持的HTTP请求方法列表,例如"{RequestMethod.GET, RequestMethod.POST}"},默认情况下所支持的方法与控制器映射的方法相同。
    1. allowCredentials,是否允许跨域请求的cookie随请求发送(不单单是Cookie),配置的值展示在响应头Access-Control-Allow-Credentials中。
    1. maxAge,返回结果可以用于缓存的最长时间,单位是秒,默认是1800秒,也就是30分钟,展示在响应头Access-Control-Max-Age中,将此设置为一个合理的值可以减少浏览器所需的预请求/响应交互的数量。
@CrossOrigin(maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {

    @CrossOrigin(origins = "http://domain2.com")
    @GetMapping("/{id}")
    public Account retrieve(@PathVariable Long id) {
        // ...
    }

    @DeleteMapping("/{id}")
    public void remove(@PathVariable Long id) {
        // ...
    }
}

加在类上,表示整个Controller都支持跨域访问;加到方法上,则是针对具体的方法。而如果要使整个application都支持的话,可以进行如下配置:

@Override
public void addCorsMappings(CorsRegistry registry) {
    registry.addMapping("/**");
}

当然,我们也可以根据需要修改对应属性,以及配置适用于特定的路径模式的跨域请求:

@Override
public void addCorsMappings(CorsRegistry registry) {
    registry.addMapping("/api/**")
        .allowedOrigins("http://domain2.com")
        .allowedMethods("PUT", "DELETE")
            .allowedHeaders("header1", "header2", "header3")
        .exposedHeaders("header1", "header2")
        .allowCredentials(false).maxAge(3600);
}

这里推荐查看官网文档,文档上有更详细的例子:https://docs.spring.io/spring/docs/4.3.14.RELEASE/spring-framework-reference/html/cors.html
有关跨域,还可以参考以下链接:
Cross-Origin Resource Sharing
Spring中@CrossOrigin的作用

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,585评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,724评论 6 342
  • 紧接着上文,我们来学习mvc标签。 Spring版本:4.3.14。 6.mvc 命名空间   mvc命名空间内的...
    骑着乌龟去看海阅读 1,751评论 0 2
  • 前半周连续几次会议后,周五最后只安排了下午的项目会议就在下榻酒店的顶楼,整个项目参与成员似乎都是大叔级别的,不管是...
    江南暮烟阅读 432评论 0 0
  • 1 引子 2017年12月23日,“来讲”演讲俱乐部演讲活动,主题——“自由”。我选择了一直很想分享的保险的话题。...
    徐丹的写作课阅读 566评论 0 0