Kaze
Kaze
Published on 2022-03-06 / 90 Visits
0
0

Spring Web 全栈(二)

  • Spring Validation:表单数据验证

    • 依赖:

      <dependency>
        <groupId>jakarta.validation</groupId>
        <artifactId>jakarta.validation-api</artifactId>
        <version>2.0.1</version>
      </dependency>
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-validation</artifactId>
      </dependency>
      
    • 注解

      • @NotNull
        

        不允许为 null 对象

      • @AssertTrue
        

        是否为 true

      • @Size
        

        约定字符串的长度

      • @Min
        

        字符串的最小长度

      • @Max
        

        字符串的最大长度

      • @Email
        

        是否是邮箱格式

      • @NotEmpty
        

        不允许为null或者为空,可以用于判断字符串、集合,比如 Map、数组、List

      • @NotBlank
        

        不允许为 null 和 空格

     @NotEmpty(message = "名称不能为 null")
        private String name;
    
        @Min(value = 18, message = "你的年龄必须大于等于18岁")
        @Max(value = 150, message = "你的年龄必须小于等于150岁")
        private int age;
    
        @NotEmpty(message = "邮箱必须输入")
        @Email(message = "邮箱不正确")
        private String email;
    
    • 执行校验

       @PostMapping("/user/save")
          public String saveUser(@Valid User user, BindingResult errors) {
              if (errors.hasErrors()) {
                  // 如果校验不通过,返回用户编辑页面
                  return "user/addUser";
              }
              // 校验通过,返回成功页面
              return "redirect:/user/list.html";
              //redirect: 这个用于跳转到某一个页面网址,如果是同一个域名,你可以省略域名,直接写 path
          }
      
    • <form action="/user/save" th:object="${user}" method="POST">
        <div th:classappend="${#fields.hasErrors('age')} ? 'error' : ''">
        <label>年龄:</label>
        <input type="text" th:field="*{age}" />
        <p th:if="${#fields.hasErrors('age')}" th:errors="*{age}"></p>
        </div>
      </form>
      
      //th:object 用于替换对象,使用了这个就不需要每次都编写 user.xxx,可以直接操作 xxx 了
      //th:classappend 用于给标签添加class,支持我们动态的管理样式
      //th:errors 显示出错误信息(注解中的message属性值)
      //th:field 显示上一次输入的内容
      
  • Thymeleaf 布局(Layout):合并渲染HTML

    • 片段

      <!DOCTYPE html>
      <html lang="en" xmlns:th="http://www.thymeleaf.org"
            th:replace="layout">
      <div th:fragment="content">
          //内容
      </div>
      </html>
      
    • 模板

      <!DOCTYPE html>
      <html lang="en" xmlns:th="http://www.thymeleaf.org">
      <head>
          <meta charset="UTF-8">
          <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
          <title>布局</title>
      </head>
      <body>
      <header class="header">
          //模板内容
      </header>
      
      <div class="container" th:include="::content">页面正文内容</div>//插入片段的地方
      
      <footer class="footer">
          //模板内容
      </footer>
      </body>
      </html>   
      
  • 迭代器:用于遍历集合对象

    Iterator<Book> itr = books.iterator();
    while (itr.hasNext()) {
        Book book = itr.next();
        if (book.getId() == bookId) {
            itr.remove();
        }
    }
    
  • Spring Boot ComponentScan

    • 加了 @SpringBootApplication 注解的类是启动类,是整个系统的启动入口,Spring Boot 框架会默认扫描 fm.douban.app 包(启动类所在的包)及其所有子包(fm.douban.app.*fm.douban.app.*.*)进行解析。但 fm.douban.servicefm.douban.service.impl 不是 fm.douban.app 的子包(平级),所以不会自动扫描,也不会自动实例化 Bean,自然不会实例化 SongServiceImpl。所以报错了

    • 解决办法:为启动类注解 @SpringBootApplication 加一个参数,告知系统需要额外扫描的包

      @SpringBootApplication(scanBasePackages={"fm.douban.app", "fm.douban.service"})
      public class AppApplication {
        public static void main(String[] args) {
          SpringApplication.run(AppApplication.class, args);
        }
      }
      
    • 如果不是 Spring Boot 启动类,可以使用独立的注解 @ComponentScan ,作用也是一样的,用于指定多个需要额外自动扫描的包

      @ComponentScan({"fm.service", "fm.app"})
      public class SpringConfiguration {
        ... ...
      }
      
    • 使用 @RestController 的类,所有方法都不会渲染 Thymeleaf 页面,而是都返回数据。等同于使用 @Controller 的类的方法上添加 @ResponseBody 注解,效果是一样的。

  • Spring Boot Logger

    • 配置:修改 Spring Boot 系统的标准配置文件: application.properties(在项目的 src/main/resources/ 目录下),增加日志级别配置:

      logging.level.root=info
      //表示所有日志(root)都为 info 级别。
      
      logging.level.fm.douban.app=info
      //也可以为不同的的包定义不同的级别
      
      logging.level.root=error意味着不输出更低优先级的 WARN、INFO、DEBUG 日志, 只输出ERROR 日志。
      
    • 编码:配置完成后,编码很简单,只需要实例化日志对象即可打印日志了

      public class SongListControl {
          private static final Logger LOG = LoggerFactory.getLogger(SongListControl.class);
      
          @PostConstruct
          public void init(){
              LOG.info("SongListControl 启动啦");
          }
      }
      //定义类变量 LOG 的语句属于固定写法,只需要在不同的类中,修改 getLogger() 方法参数为当前的类名即可。这样就能识别每段日志的来源。
      
  • Spring Boot Properties

    • 配置文件格式:每一行是一条配置项:配置项名称=配置项值

    • 自定义配置项:

      我们可以在 application.properties 配置文件中加入自定义的配置项。如:
      song.name=God is a girl
      
      使用配置项
      @Value("${song.name}")
          private String songName;
      
      代码中使用配置项,application.properties 文件必须有配置,缺少了就会报错;但 application.properties 文件中的配置没有被代码使用,则没关系。就是说,多了没事,少了就报错 。
      
  • Cookie

    • 读Cookie:

      public Map index(HttpServletRequest request) {
        Map returnData = new HashMap();
        returnData.put("result", "this is song list");
        returnData.put("author", songAuthor);
      
        Cookie[] cookies = request.getCookies();
        returnData.put("cookies", cookies);
      
        return returnData;
      }
      
    • 使用注解读取 cookie:如果知道 cookie 的名字,就可以通过注解的方式读取,不需要再遍历 cookie 数组了,更加方便。

      control 类的方法增加一个 @CookieValue("xxxx") String xxxx 参数即可,注意使用时要填入正确的 cookie 名字。

    • 写Cookie

      public Map index(HttpServletResponse response) {
        Map returnData = new HashMap();
        returnData.put("result", "this is song list");
        returnData.put("name", songName);
      
        Cookie cookie = new Cookie("sessionId","CookieTestInfo");
        // 设置的是 cookie 的域名,就是会在哪个域名下生成 cookie 值
        cookie.setDomain("youkeda.com");
        // 是 cookie 的路径,一般就是写到 / ,不会写其他路径的
        cookie.setPath("/");
        // 设置cookie 的最大存活时间,-1 代表随浏览器的有效期,也就是浏览器关闭掉,这个 cookie 就失效了。
        cookie.setMaxAge(-1);
        // 设置是否只能服务器修改,浏览器端不能修改,安全有保障
        cookie.setHttpOnly(false);
        response.addCookie(cookie);
      
        returnData.put("message", "add cookie successfule");
        return returnData;
      }
      
  • Spring Session

    • 读取session

      public Map index(HttpServletRequest request, HttpServletResponse response) {
        Map returnData = new HashMap();
        returnData.put("result", "this is song list");
      
        // 取得 HttpSession 对象
        HttpSession session = request.getSession();
        // 读取登录信息,因为getAttribute返回的是object对象所以要转化
        UserLoginInfo userLoginInfo = (UserLoginInfo)session.getAttribute("userLoginInfo");
        if (userLoginInfo == null) {
          // 未登录
          returnData.put("loginInfo", "not login");
        } else {
          // 已登录
          returnData.put("loginInfo", "already login");
        }
      
        return returnData;
      }
      //登录信息实例对象UserLoginInfo因为要在网络上传输,就必须实现序列化接口 Serializable ,否则不实现的话会报错。
      
    • 写入session

      public Map loginMock(HttpServletRequest request, HttpServletResponse response) {
        Map returnData = new HashMap();
      
        // 假设对比用户名和密码成功
        // 仅演示的登录信息对象
        UserLoginInfo userLoginInfo = new UserLoginInfo();
        userLoginInfo.setUserId("12334445576788");
        userLoginInfo.setUserName("ZhangSan");
        // 取得 HttpSession 对象
        HttpSession session = request.getSession();
        // 写入登录信息
        session.setAttribute("userLoginInfo", userLoginInfo);
        returnData.put("message", "login successfule");
      
        return returnData;
      }
      
    • Spring Session 配置

      • SpringBoot提供了编程式的配置方式,主要用于配置 Bean,方法是在类上添加 @Configuration 注解,就表示这是一个配置类,系统会自动扫描并处理。

        在方法上添加 @Bean 注解,表示把此方法返回对象实例注册成 Bean

      • session配置

        • 先增加依赖

          <!-- spring session 支持 -->
          <dependency>
              <groupId>org.springframework.session</groupId>
              <artifactId>spring-session-core</artifactId>
          </dependency>
          
        • 在类上额外添加一个注解:@EnableSpringHttpSession ,开启 session 。然后,注册两个 bean

          • CookieSerializer:读写 Cookies 中的 SessionId 信息
          • MapSessionRepository:Session 信息在服务器上的存储仓库。
        • @Configuration
          @EnableSpringHttpSession
          public class SpringHttpSessionConfig {
            @Bean
            public CookieSerializer cookieSerializer() {
              DefaultCookieSerializer serializer = new DefaultCookieSerializer();
              serializer.setCookieName("JSESSIONID");
                  // 用正则表达式配置匹配的域名,可以兼容 localhost、127.0.0.1 等各种场景
              serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$");
              serializer.setCookiePath("/");
              serializer.setUseHttpOnlyCookie(false);
              // 最大生命周期的单位是秒
              serializer.setCookieMaxAge(24 * 60 * 60);
              return serializer;
            }
          
            // 当前存在内存中
            @Bean
            public MapSessionRepository sessionRepository() {
              return new MapSessionRepository(new ConcurrentHashMap<>());
            }
          }
          
  • Spring Request 拦截器

    • 用处:用于统一处理相同逻辑。如在实际的项目中,会有大量的页面功能是需要判断用户是否登录的。例如电商的网站,订单、购物车、管理收货地址等等,都需要登录。那么让每一个页面都判断是否登录、未登录跳转到登录页,就太繁琐了,也不利于维护。

    • 创建拦截器

      public class Interceptor implements HandlerInterceptor {
      
        // Controller方法执行之前
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
      
          // 只有返回true才会继续向下执行,返回false取消当前请求
          return true;
        }
      
        //Controller方法执行之后
        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
            ModelAndView modelAndView) throws Exception {
      
        }
      
        // 整个请求完成后(包括Thymeleaf渲染完毕)
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
      
        }
      }
      
    • 配置拦截器

      @Configuration
      public class WebAppConfigurer implements WebMvcConfigurer {
      
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
          // 多个拦截器组成一个拦截器链
          // 仅演示,设置所有 url 都拦截
          registry.addInterceptor(new Interceptor()).addPathPatterns("/**");
        }
      }
      //也可以调用 excludePathPatterns() 方法排除某些 URL
      //常用 addPathPatterns("/**") 表示拦截所有的 URL
      
  • HttpServletRequest 提供了 setAttribute()getAttribute() 方法,可以在当前的这次请求中设置和读取额外的属性数据

  • @RequestMapping 注解放在 control 类上,表示为整个类的所有方法统一加上一个前缀

    如:@RequestMapping(path="app")

  • 页面跳转

    String loginPageUrl = "/login";
    response.sendRedirect(loginPageUrl);
    
  • MongoDB

    • 安装:先安装Docker再安装MongoDB

      sudo docker run -it --name mongo -p 27017:27017 -d mongo//安装
      sudo docker exec -it mongo mongo admin//启动
      
      db.createUser({ user:'admin',pwd:'123456',roles:[ { role:'root', db: 'admin'}]});//创建管理员账户
      
      db.createUser({ user:'xxxx',pwd:'xxxxxx',roles:[{ role:'root', db: 'admin'},{ role:'dbAdmin', db: 'practice'}]});//创建读写账户
      
      db.auth('admin', '123456')//登录账户
      use 名字//创建并进入数据库
      exit//退出
      sudo docker ps//查看容器
      
    • 依赖:可在创建工程时选中Spring Data MongoDB,如果是已经创建的工程,可以手动增加依赖库

      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-data-mongodb</artifactId>
      </dependency>
      
    • 配置

      # 购买的云服务器的公网 IP
      spring.data.mongodb.host=
      # MongoDB 服务的端口号
      spring.data.mongodb.port=27017
      # 创建的数据库及用户名和密码
      spring.data.mongodb.database=practice
      spring.data.mongodb.username=
      spring.data.mongodb.password=
      
    • 数据增删改查

      • 对数据库的操作一定要放在 @Service 类中,而不是放在 @Controller 类中;且 @Controller 类可以调用 @Service 类的方法

      • 新增数据

         @Autowired
          private MongoTemplate mongoTemplate;
        
          public void test() {
            Song song = new Song();
            song.setId("001");//设置主键
            song.setSubjectId("s001");
            song.setLyrics("...");
            song.setName("成都");
        
            mongoTemplate.insert(song);
        
      • 查询数据

        //findById() 方法第 1 个参数就是主键 id,第 2 个参数是具体的类,写法是 类名.class 
        mongoTemplate.findById(songId, Song.class)
        
        //多条件查询
        //组合条件,根据或(or)、且(and)的关系进行组合,多个子条件对象组合成一个总条件对象:
        Criteria criteria = new Criteria();
        criteria.orOperator(criteria1, criteria2);//criteria.andOperator(criteria1, criteria2);    
        Query query = new Query(criteria);
        query.limit(10);//限定查询量
        List<Song> songs = mongoTemplate.find(query, Song.class);
        /*orOperator()和andOperator()的参数,都可以输入多个子条件,也可以输入子条件数组
        List<Criteria> subCris = new ArrayList();
        criteria.andOperator(subCris.toArray(new Criteria[]{}));*/
        
      • 修改数据

        // 修改 id=1 的数据
        //单一条件查询
        Query query = new Query(Criteria.where("条件字段名").is("条件值"));
        
        // 把歌名修改为 “new name”
        Update updateData = new Update();
        updateData.set("name", "new name");
        
        // 执行修改,修改返回结果的是一个对象
        UpdateResult result = mongoTemplate.updateFirst(query, updateData, Song.class);
        // 修改的记录数大于 0 ,表示修改成功
        System.out.println("修改的数据记录数量:" + result.getModifiedCount());
        
      • 删除数据

        Song song = new Song();
        song.setId(songId);
        
        // 执行删除
        DeleteResult result = mongoTemplate.remove(song);//用对象来表示查询条件。作为服务接口,使用对象做参数,具备比较好的扩展性和兼容性。
        // 删除的记录数大于 0 ,表示删除成功
        System.out.println("删除的数据记录数量:" + result.getDeletedCount());
        //创建一个对象并设置好属性值,作为删除的条件,符合条件的数据都将被删除。可以设置更多的属性值来提高精确性,但通过主键来删除数据,是保证不误删的一个比较好的办法。
        
      • 分页查询

        Pageable pageable = PageRequest.of(0 , 20);//PageRequest.of() 方法第一个参数是页码,注意从 0 开始计数,第一页的值是 0 ;第二个参数是每页的数量。
        query.with(pageable);
        // 总数
        long count = mongoTemplate.count(query, Song.class);
        // 根据结果、分页条件、总数三个数据,构建分页器对象。
        Page<Song> pageResult = PageableExecutionUtils.getPage(songs, pageable, new LongSupplier() {
          @Override
          public long getAsLong() {
            return count;
          }
        });
        

Comment