学习SSM的记录(六)-- Spring MVC

目录

[Spring MVC 简介和体验](#Spring MVC 简介和体验)

[Spring MVC原理简单解析](#Spring MVC原理简单解析)

[Spring MVC涉及的组件](#Spring MVC涉及的组件)

[Spring MVC 快速体验](#Spring MVC 快速体验)

[Spring MVC 接收数据](#Spring MVC 接收数据)

访问路径设置

接收参数(重点)

param和json参数比较

param参数接收

路径参数接收

json参数接收

@EnableWebMvc注解

接收Cookie和请求头数据

原生Api对象和共享域对象操作

[Spring MVC 响应数据](#Spring MVC 响应数据)

handler方法分析

页面跳转控制

快速返回模板视图

转发和重定向

返回JSON数据(重点)

返回静态资源处理

RestFul风格设计

RESTFul风格特点

[Spring MVC 其他拓展](#Spring MVC 其他拓展)

全局异常处理机制

异常处理的两种方式

基于注解异常声明异常处理

拦截器使用

拦截器概念

拦截器使用

参数校验


  • SSM为SpringFramework + SpringMVC + MyBatis
  • Spring的3大特性:IoC(控制反转),DI(依赖注入),AOP(面向切面编程)。
  • 框架 = jar + 配置文件

Spring MVC 简介和体验

Spring Web MVC 是一个基于Servlet API 构建的原始Web框架,一开始包含在Spring Framework下,目前普遍被选为JavaEE项目表述层开发的首选。

Spring MVC框架的两个核心功能:1.简化前端参数接收(形参列表)

2.简化后端数据响应(返回值)

Spring MVC原理简单解析

Spring MVC 框架内主要工作的有:

  • DispatcherServlet:处理所有请求,用户的所有请求都由它接收
  • HandlerMapping:缓存handler方法和地址,根据地址(如:/user/login)查找项目中的方法(如:UserController中的login方法)
  • HandlerAdapter:适配器,真正进行参数和响应简化
  • 视图解析器:查找视图页面,如果要返回的页面如:/WEB-INF/html/index.html,只需要返回"index",它可以帮我们添加前缀和后缀,并查找页面信息并返回。

执行流程:

1.用户发送请求

2.请求到达DispatcherServlet

3.DispatcherServlet根据请求地址到HandlerMapping查找方法

4.HandlerMapping查找到handler方法并返回方法信息到DispatcherServlet。

5.DispatcherServlet根据handler方法信息将参数信息和方法信息发送到HandlerAdapter

6.HandlerAdapter进行简化参数处理,并调用handler方法

7.handler方法执行并返回数据给HandlerAdapter

8.HandlerAdapter接收数据并发送给DispatcherServlet

9.DispatcherServlet接收数据,如果接收的是页面地址则进行 操作10 ,不是则直接将数据返回给用户

10.视图解析器为页面添加前后缀并查找页面信息后返回给DispatcherServlet,DispatcherServlet将页面信息返回给用户。

Spring MVC涉及的组件

1.DispatcherServlet:Spring MVC提供,需要配置web.xml(Web项目配置文件)才能生效。

2.HandlerMapping:Spring MVC提供,需要进行IOC配置,将它加入到IOC容器才能生效,它内部缓存handler(controller的方法)和handler访问路径数据。

3.HandlerAdapter:Spring MVC提供,需要进行IOC配置,将它加入到IOC容器才能生效,它可以处理请求参数和处理响应数据。

4.Handler:handler又叫处理器,它是controller层方法的简称,由我们自己定义,接收参数,调用业务方法,返回数据。

5.ViewResolver(视图解析器):Spring MVC提供,需要进行IOC配置,将它加入到IOC容器才能生效,简化视图页面查找,前后端分离后,后端只需要返回JSON数据,不再需要视图解析器。

Spring MVC 快速体验

场景:项目启动后,用户访问/springmvc/hello服务器响应"hello springmvc!!"

1.创建项目ssm-springmvc-quick并转为web项目

2.导入项目所需的依赖:

  • ioc:spring-context
  • webmvc:spring-webmvc
  • servlet:jakarta.jakartaee-web-api

3.创建一个controller类:HelloController

复制代码
@Controller
public class HelloController {

    @RequestMapping("springmvc/hello")//用户访问地址
    @ResponseBody//直接返回数据,不需要经过视图解析器
    public String hello(){
        System.out.println("Hello , Spring MVC!");
        return "hello springmvc";
    }
}

4.创建spring配置类

复制代码
//将controller配置到ioc容器
//将handlerMapping handlerAdapter加入到ioc容器
@Configuration
@ComponentScan("com.qiu.controller")
public class MvcConfig {
    @Bean
    public RequestMappingHandlerMapping handlerMapping(){
        return new RequestMappingHandlerMapping();
    }
    @Bean
    public RequestMappingHandlerAdapter handlerAdapter(){
        return new RequestMappingHandlerAdapter();
    }
}

5.初始化ioc容器,设置dispatcherServlet访问路径(就是想要访问项目应该有的地址前缀)

以前我们需要在web.xml下操作:

复制代码
<servlet>
    <servlet-name>ds</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>ds</servlet-name>
    <url-pattern>/</url-pattern><!--访问路径-->
</servlet-mapping>

但现在可以创建一个用来初始化web项目的类,让他继承

复制代码
AbstractAnnotationConfigDispatcherServletInitializer

,项目启动后会自动扫描他,该类的作用是扫描Spring配置类来初始化ioc容器和设置DispatcherServlet的访问路径

复制代码
//继承AbstractAnnotationConfigDispatcherServletInitializer类
//用来替换web.xml,会被web项目自动加载,会初始化ioc容器,会设置dispatcherServlet的访问地址
public class SpringMvcInit extends AbstractAnnotationConfigDispatcherServletInitializer {
    //service mapper 层的ioc容器
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[0];
    }
    //设置spring配置类 springmvc controller的ioc容器
    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[]{MvcConfig.class};
    }
    //设置springmvc自带servlet的访问地址
    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }
}

6.部署项目

7.启动项目并访问项目localhost:8080/springmvc/hello

Spring MVC 接收数据

访问路径设置

之前:@WebServlet("必须以 / 开头") /user/login

现在:@RequestMapping(不必须以 / 开头) /user/login user/login /user/login/

1.精准地址 :[一个 | 多个] /user/login | {"/user/login","/user/login2"}

2.模糊地址:* 一层模糊 ** 任意层模糊

/user/* -> /user/a,/user/aaa 可以;/user/a/b 不可以。

/user/** -> /user/a,/user/a/b,/user/a/a/a/a

3.该注解可以使用在类上或方法上,最终访问路径为:类地址 + 方法地址

4.请求方式指定:

请求方式主要有:GET,POST,PUT,DELETE

方式1:@RequestMapping("login",method=RequestMethod.GET)

方式2:@GetMapping("login") ,该注解只能用在方法上

不指定时默认所有请求方法都可以

多个请求方法@RequestMapping("a",method={RequestMethod.GET,RequestMethod.POST})

如果有类注解,方法注解内可以不写路径,该方法的访问路径默认为类注解的路径

接收参数(重点)

param和json参数比较

1.参数编码

param参数会被编译为ASCII码,如name=john doe,会被编译为name=john%20doe;而JSON参数会被编译为UTF-8。

2.参数顺序

param参数没有顺序限制,但JSON参数有顺序限制。

3.数据类型

param类型仅支持字符串类型、数值类型和布尔类型;JSON参数则支持更复杂的类型:数组,对象等。

4.嵌套性

param参数不支持嵌套,JSON参数支持

param参数接收

1.直接接收

如果形参数名和传递参数名相同,即可自动接收。

2.@RequestParam注解

可以使用该注解将Servlet请求参数绑定到controller中的方法的参数上。

使用场景: 指定绑定的请求参数名,要求参数必须传递,为请求参数提供默认值。

演示

复制代码
@Controller
@RequestMapping("user")
public class UserController {
    /**
     * 直接接收
     * 请求地址:/user/data1?name=qiu&age=18
     * 1.方法参数名和请求参数名一致,2.请求参数可以不传时
     */
    @ResponseBody
    @RequestMapping("data1")
    public String test(String name, int age){
        System.out.println("name = " + name);
        System.out.println("age = " + age);
        return "name="+name+" age="+age;
    }
    /**
     * 注解传参
     * 指定任意的请求参数名,默认要求必须传递,
     *                   如果设置为不必须传递则要设置默认。
     *      /user/data2?account=qiu&age=18
     */
    @ResponseBody
    @RequestMapping("data2")
    public String test2(@RequestParam("account") String name,
                        @RequestParam(required = false,defaultValue = "1") int age){
        System.out.println("name = " + name + ", age = " + age);
        return "name=" + name +"&age="+age;
    }
    /**
     * 特殊值
     *  一个属性传多个值,直接使用集合接收
     *  必须使用@RequestParam,不然可能会把第一个ids当作集合ids直接传值,类型不同会报错
     *      /user/data3?ids=1&ids=2&ids=3
     */
    @ResponseBody
    @RequestMapping("data3")
    public String test3(@RequestParam("ids")List<String> ids){
        System.out.println("ids = " + ids);
        return ids.toString();
    }
    /**
     * 使用实体类接收值
     * 创建User类 类属性有name 和 age,必须要有set/get方法
     *      属性名必须等于参数名
     *      /user/data4?name=qiu&age=18
     */
    @ResponseBody
    @RequestMapping("data4")
    public String test4(User user){
        System.out.println("user = " + user);
        return user.toString();
    }
}

测试

路径参数接收

像/user/{name}/18,如果我们想接收name的值,则可以使用路径参数接收

步骤:1、设置动态路径

2、接收动态路径参数

复制代码
//  /user/data5/qiu/123
@ResponseBody
@RequestMapping("data5/{name}/{password}")//设置动态路径
//获取路径参数
public String test5(@PathVariable("name") String username,@PathVariable String password){
    return username+password;
}
json参数接收

前端传递JSON数据时,SpringMVC框架可以使用@RequestBody来将JSON数据转换为Java对象。

Java对象Person

复制代码
@Data
public class Person {
    private String name;
    private int age;
    private String gender;
}

Controller类PersonController

复制代码
@Controller
@RequestMapping("json")
public class PersonController {

    @RequestMapping("person")
    @ResponseBody
    public String test(@RequestBody Person person){
        return person.toString();
    }
}

发送请求

结果会报415错误,因为java原生的api只能接收路径传参和param参数,不支持json参数

解决方法:1、导入json处理的依赖 2、为handlerAdapter配置json转化器

添加依赖

复制代码
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.15.0</version>
</dependency>

在spring配置类上添加**@EnableWebMvc**:为handlerAdapter配置了json转化器

这样请求就发送成功了

@EnableWebMvc注解

在spring配置类上添加该注解相当于在springxml配置文件添加了<mvc:annotation-driven/>

而<mvc:annotation-driven/>在底层会自动帮我们把json处理器添加到一个HandlerAdapter上,再把这个handlerAdapter和一个handlerMapping添加到ioc容器内。

因此当我们添加该注解后就不需要在spring配置类中手动添加HandlerAdapter和HandlerMapping了。

接收Cookie和请求头数据

在参数前添加@CookieValue和@RequestHeader就行

复制代码
public String method(@CookieValue("cookie") String value,                      @RequestHeader("header")String value2){
      ...
}

原生Api对象和共享域对象操作

想要使用原生Api对象可以直接通过参数获得

复制代码
    @Autowired
    private ServletContext context;
    public String getApi(HttpServletRequest request,
                         HttpServletResponse response,
                         HttpSession session){
//       要获取ServletContext
//       ServletContext [1.最大的配置文件,2.全局最大的共享域,3.核心api getRealPath]
//        方式1:通过request,session获取
        ServletContext servletContext = request.getServletContext();
        ServletContext servletContext1 = session.getServletContext();
//        方式2:声明一个全局变量,然后再上面添加一个@Autowire
//                (ServletContext在程序启动时会自动添加到ioc容器内)
        return "";
    }

Spring MVC 响应数据

handler方法分析

handler方法其实就是我们自己创建的controller类下的各种方法

接收请求数据,我们通过handler方法的形参列表

返回数据响应,我们通过return关键字

页面跳转控制

快速返回模板视图

当使用前后端分离模式(当前主流)时,该功能不再被需要。

要把jsp等视图返回给用户,需要使用视图解析器。

因此要先把视图解析器加入ioc容器

复制代码
@Configuration
@ComponentScan("com.qiu.controller")
@EnableWebMvc
//WebMvcConfigurer 可以快速配置springmvc的组件
public class MvcConfig implements WebMvcConfigurer {

//    将视图解析器加入到ioc容器内,
//    registry.jsp("/WEB-INF/views/",".jsp") 为handler返回的视图路径添加前后缀,其底层就是字符串拼接
    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.jsp("/WEB-INF/views/",".jsp");
    }
}

编写index.jsp文件

复制代码
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
<%--    ${data}意思是在request域中获取key=data的value,在request.setAttribute("data","hello jsp")--%>
    <font color="red">${data}</font>
</body>
</html>

编写handler方法

复制代码
@Controller
@RequestMapping("jsp")
public class HelloController {

    @GetMapping("index")
    public String hello(HttpServletRequest request){
        //在请求域中设置data:hello jsp,为index.jsp文件内的${data}赋值
        request.setAttribute("data","hello jsp");
        //此handler没有@ResponseBody,
        //  所以将把“index”与视图解析器设置的前后缀拼接并查找文件并返回给用户
        return "index";
    }

}

这样用户就可以获取到index.jsp

转发和重定向

直接通过以下代码和注释学习

复制代码
@GetMapping("index")
public String hello(HttpServletRequest request){
    //在请求域中设置data:hello jsp,为index.jsp文件内的${data}赋值
    request.setAttribute("data","hello jsp");
    //此handler没有@ResponseBody,
    //  所以将把“index”与视图解析器设置的前后缀拼接并查找文件并返回给用户
    return "index";
}

/**
 * 转发和重定向依然不能添加@ResponseBody
 * 要转发时,需要在返回的地址前添加 forward:
 * 要重定向时,需要在返回的地址前添加 redirect:
 */
@GetMapping("forward")
public String forward(){
    return "forward:/jsp/index";
}
//原本重定向的地址如果是项目内需要包含根路径,
//      即 http://localhost:8080/mvcpro/jsp/index 应该返回 redirect:/mvcpro/jsp/index
//      但springmvc内部做了优化,不需要根路径
@GetMapping("redirect")
public String redirect(){
    return "redirect:/jsp/index";
}

@GetMapping("baidu")
public String baidu(){
    return "redirect:http://www.baidu.com";
}

返回JSON数据(重点)

接收JSON和返回JSON都需要导入jackson-databind依赖

@ResponseBody:返回JSON的注解,添加到类或方法上,不走视图解析器直接把数据返回给前端。

当我们想要返回JSON数据给前端时,先导入依赖,把JSON解析器添加到ioc容器中(使用@EnableWebMvc),然后在handler方法上添加@ResponseBody注解,最后直接把pojo类返回就行了,springmvc会自动把该类转换为JSON数据再发送给前端。

pojo类

复制代码
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private String name;
    private String gender;
}

handler方法

复制代码
@Controller
@ResponseBody
@RequestMapping("json")
public class JsonController {

    @GetMapping("user")
    public User json(){
        return new User("秋","男");
    }

}

返回静态资源处理

如果项目结构像这样,

我们无法通过 .../images/OIP-C.jpg访问静态资源。(jsp属于动态资源)

原因:DispatcherServlet会接收所有请求,包括对静态资源的请求,并且他只会在HandlerMapping中根据路径查找对应的handler方法,而静态资源没有对应的handler方法。

解决办法:需要我们开启静态资源查找:

还是在 WebMvcConfigurer 接口下,有个 configureDefaultServletHandling 方法

复制代码
//相当与开启了<mvc:default-servlet-handler/>
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
    configurer.enable();//开启
}

开启后dispatcherServlet在HandlerMapping中没有找到资源,会再拿着路径查找有没有对应的静态资源。

RestFul风格设计

RestFul:Http协议的标准使用方案和风格。

前端往后端发送请求时,需要考虑:

1.路径如何设计(如:/user/add?) 2.要使用哪种传递参数方案(param?json?path?)

3.要使用哪种请求方式(get?post?delete?put?)

RestFul就是解决这些问题的一种方法。

RESTFul风格特点

1.每一种URI代表一种资源(URI是名词)

设计URI时尽量不使用动词,如:要新增用户不用:POST /user/add,而是使用:POST /user; 要删除用户不用:DELETE /user/delete?id=1,而是使用:DELETE /user/1

2.客户端使用GET,POST,PUT,DELETE 4个表示操作方式的动作来对服务器资源进行操作:

GET用来获取资源,POST用来新建资源,PUT用来更新资源,DELETE用来删除资源。

3.资源使用xml或JSON

Spring MVC 其他拓展

全局异常处理机制

异常处理的两种方式

异常处理一般分为:编程式异常处理(使用trycatch手动显示地处理) 和 声明式异常处理(将异常处理的逻辑从具体业务逻辑中脱离,通过配置等方式进行统一的管理和处理)。

基于注解异常声明异常处理

1.声明异常处理控制器类

2.声明异常处理handler方法

java 复制代码
//该注解 = @ControllerAdvice + @ResponseBody
//@ControllerAdvice 代表当前类的异常处理controller
//异常发生时,会走此类写的handler,
@RestControllerAdvice
public class GlobalExceptionHandler {

    //发送异常 -》 ControllerAdvice -》@ExceptionHandler -》根据异常类调用方法

    @ExceptionHandler(ArithmeticException.class)
    public Object ArithmeticExceptionHandler(ArithmeticException e){
        //自定义处理异常
        return null;
    }
    @ExceptionHandler(Exception.class)
    public Object ExceptionHandler(Exception e){
        return null;
    }


}

拦截器使用

拦截器概念

Filter过滤器

在javaweb项目中,可以使用Filter过滤器,当请求来到服务器时,会先经过过滤器的处理(登录保护,编码格式,权限处理),再到对应的目标类。

但SpringMVC中,Filter过滤器就不适用了,SpringMVC使用DispatcherServlet接收请求,当我们设置了Filter时,请求会在到达DispatcherServlet前经Filter处理,而SpringMVC内部细化流程Filter无法拦截。

HandlerInterceptor 拦截器(SpringMVC环境下推荐使用)

SpringMVC提供了HandlerInterceptor拦截器,它会在handlerAdapter调用handler之前和之后拦截以及整体处理之后拦截。

拦截器使用

1.创建拦截器类

java 复制代码
public class MyInterceptor implements HandlerInterceptor {

    //在handler执行前拦截:编码格式设置,登录保护,权限处理

    /**
     *
     * @param request   请求对象
     * @param response  响应对象
     * @param handler   我们要调用的方法对象
     * @return true 放行     false 拦截
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        return HandlerInterceptor.super.preHandle(request, response, handler);
    }
    //在handler执行后触发,没有拦截机制(方法已执行,拦截无意义)
    // modelAndView 返回的视图和共享域对象
    // 可以进行敏感词检查
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }
    //在数据返回给客户端前拦截
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }
}

2.修改配置类添加拦截器

复制代码
@Configuration
@ComponentScan("com.qiu.controller")
@EnableWebMvc
//WebMvcConfigurer 可以快速配置springmvc的组件
public class MvcConfig implements WebMvcConfigurer {

//    将视图解析器加入到ioc容器内,
//    registry.jsp("/WEB-INF/views/",".jsp") 为handler返回的视图路径添加前后缀,其底层就是字符串拼接
    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.jsp("/WEB-INF/views/",".jsp");
    }

    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //配置方案1:拦截所有请求
        //registry.addInterceptor(new MyInterceptor());
        //配置方案2:指定地址拦截        /**:拦截路径下所有请求
        //registry.addInterceptor(new MyInterceptor())
        //                .addPathPatterns("/user/**");
        //配置方案3:排除拦截   
        registry.addInterceptor(new MyInterceptor())
                          .addPathPatterns("/user/**")
                                .excludePathPatterns("/user/data"); 
    }
}

参数校验

场景:每次请求参数都需要判断是否为空和检查格式是否正确。

java通过了jsr303系列注解,只需要准备对应的实体类并在其属性上添加注解,当该实体类对象的属性的数据为空或者格式有误,就会报错。

使用

1.导入依赖

XML 复制代码
<!-- 校验注解 -->
<dependency>
    <groupId>jakarta.platform</groupId>
    <artifactId>jakarta.jakartaee-web-api</artifactId>
    <version>9.1.0</version>
    <scope>provided</scope>
</dependency>
        
<!-- 校验注解实现-->        
<!-- https://mvnrepository.com/artifact/org.hibernate.validator/hibernate-validator -->
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>8.0.0.Final</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.hibernate.validator/hibernate-validator-annotation-processor -->
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator-annotation-processor</artifactId>
    <version>8.0.0.Final</version>
</dependency>

2.应用

java 复制代码
@Data
public class User {
    //age   1 <=  age < = 150
    @Min(10)
    private int age;

    //name 3 <= name.length <= 6
    @Length(min = 3,max = 10)
    private String name;

    //email 邮箱格式
    @Email
    private String email;

}

3.handler方法标记

java 复制代码
@RestController
@RequestMapping("user")
public class UserController {

    /**
     * @Validated 代表应用校验注解! 必须添加!
     */
    @PostMapping("save")
    public Object save(@Validated @RequestBody User user,
                       //在实体类参数和 BindingResult 之间不能有任何其他参数, BindingResult可以接受错误信息,避免信息抛出!
                       BindingResult result){
       //判断是否有信息绑定错误! 有可以自行处理!
        if (result.hasErrors()){
            System.out.println("错误");
            String errorMsg = result.getFieldError().toString();
            return errorMsg;
        }
        //没有,正常处理业务即可
        System.out.println("正常");
        return user;
    }
}
相关推荐
问道飞鱼3 小时前
每日学习一个数据结构-B+树
数据结构·b树·学习
不染_是非3 小时前
Django学习实战篇六(适合略有基础的新手小白学习)(从0开发项目)
后端·python·学习·django
Midsummer啦啦啦3 小时前
NumPy库学习之argmax函数
学习·numpy
Mero技术博客3 小时前
第二十节:学习Redis缓存数据库实现增删改查(自学Spring boot 3.x的第五天)
数据库·学习·缓存
QuantumYou4 小时前
【对比学习串烧】 SWav和 BYOL
学习·机器学习
为暗香来4 小时前
MySQL学习(视图总结)
数据库·学习·mysql
一道秘制的小菜4 小时前
C++第十一节课 new和delete
开发语言·数据结构·c++·学习·算法
kuiini5 小时前
python学习-10【模块】
python·学习
知识分享小能手5 小时前
mysql学习教程,从入门到精通,SQL ORDER BY 子句(14)
大数据·开发语言·数据库·sql·学习·mysql·大数据开发
哦豁灬5 小时前
NCNN 学习(2)-Mat
深度学习·学习·ncnn