SpringBoot基础笔记(下)

基于Spring 5和 Spring Boot 2
开发环境 Spring Tool Suite


数据持久化

处理关系型数据库的时候,开发人员可以选择JDBC和JPA等。Spring 同时支持这两种抽象形式,能够让JDBC或JPA使用更加容易。

Spring对JDBC的支持主要来自JdbcTemplate类。


JDBC

JdbcTemplate

构建文件里需要加这个:

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-jdbc</artifactId>
		</dependency>

使用H2嵌入式数据库,还要添加H2的依赖

		<dependency>
			<groupId>com.h2database</groupId>
			<artifactId>h2</artifactId>
			<scope>runtime</scope>
		</dependency>

JDBC repository

Ingredient repository需要完成如下操作

  • 查询所有的配料信息,将它们放到一个Ingredient对象的集合中
  • 根据id查询单个Ingredient
  • 保存Ingredient对象
public interface IngredientRepository{
	Iterable<Ingredient> findAll();
	Ingredient findById(String id);
	Ingredient save(Ingredient ingredient);
}
import java.sql.ResultSet;
import java.sql.SQLException;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import com.example.demo.Ingredient;

//Spring的构造型注解 repository:仓库
@Repository
public class JdbcIngredientRepository implements IngredientRepository{

	private JdbcTemplate jdbc;
	
	//通过Autowired标注的构造器将JdbcTemplate注入进来   template:模板
	//这个构造器将jdbcTemplate赋值给一个实例变量
	//这个变量会被其他方法用来执行数据库查询和插入操作
	@Autowired
	public void jdbcIngredientRespository(JdbcTemplate jdbc) {
		this.jdbc = jdbc;
	}
	
	@Override
	public Iterable<Ingredient> findAll() {
		//::是java8 中新引入的运算符
		//可以通过 `::` 关键字来访问类的构造方法,对象方法,静态方法
		// 类名::方法名 
		return jdbc.query("select id,name,type from Ingredient", this::mapRowToIngredient);
	}

	@Override
	public Ingredient findById(String id) {
		return jdbc.queryForObject("select id,name,type from Ingredient where id=?",
				this::mapRowToIngredient,id);
		
		//this::mapRowToIngredient 相当于   下面是显式写法
		/******
		new RowMapper<Ingredient>() {
			public Ingredient mapRow(ResultSet rs,int rowNum) throws SQLException{
				return new Ingredient(
				rs.getString("id"),
				rs.getString("name"),
				Ingredient.Type.valueOf(rs.getString("type"))
				);
			}
		};
		*****/
		
	}

	@Override
	public Ingredient save(Ingredient ingredient) {
		//这里不需要将ResultSet数据映射为对象,所以updata()方法要比上面两个简单
		//因为用了lombok,所以虽然显式没有实现get  但还是有的 大概是这样
		jdbc.update("insert into Ingredient (id, name, type) values (?,?,?)",
				ingredient.getId(),
				ingredient.getName(),
				ingredient.getType().toString());
		return ingredient;
	}
	
	private Ingredient mapRowToIngredient(ResultSet rs, int rowNum) throws SQLException{
			return new Ingredient(
			rs.getString("id"),
			rs.getString("name"),
			Ingredient.Type.valueOf(rs.getString("type"))
			);
	}
}

JdbcIngredientRepository添加了@Repository注解,Spring的组件扫描会自动发现它,并会将其初始化为Spring应用上下文中的bean。

当Spring创建JdbcIngredientRepository bean的时候,会通过@Autowired标注的构造器将JdbcTemplate注入进来。
这个构造器将JdbcTemplate赋值给一个实例变量,这个变量会被其他方法用来执行数据库查询和插入操作。


在控制器中注入和使用repository


import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

import javax.validation.Valid;

import com.example.demo.Ingredient.Type;
import com.example.demo.jdbcdata.IngredientRepository;
import com.example.demo.jdbcdata.TacoRepository;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.SessionAttributes;


@Controller
@RequestMapping("/design") 
@SessionAttributes("order") 
public class DesignTacoController {
	private final IngredientRepository ingredientRepo;  
	private TacoRepository designRepo;  

	@Autowired 
	public DesignTacoController(IngredientRepository ingredientRepo,TacoRepository designRepo) {
		this.ingredientRepo = ingredientRepo;
		this.designRepo = designRepo;
	}
	
	@GetMapping
	public String showDesignForm(Model model) {
		
		List<Ingredient> ingredients = new ArrayList<>();
		ingredientRepo.findAll().forEach(i->ingredients.add(i));
		
		Type[] types = Ingredient.Type.values();
		for(Type type: types) {
			//向前台传送数据
	model.addAttribute(type.toString().toLowerCase(),filterByType(ingredients,type));
		}
		return "design";
	}
	
	//确保在模型中创建一个Order对象,但是与模型里的Taco对象不同,
	//我们需要订单信息在多个请求中都能出现,这样就能创建多个taco并将它们添加到该订单中。
	@ModelAttribute(name = "order")
	public Order order(){
		return new Order();
	}
	
	@ModelAttribute(name = "taco")
	public Taco taco() {
		return new Taco();
	}

	@PostMapping   
	public String processDesign(@Valid Taco design,	Errors errors,@ModelAttribute Order order) {
		if(errors.hasErrors()) {
			return "design";
		}
		Taco saved = designRepo.save(design);
		order.addDesign(saved);
		return "redirect:/orders/current";
	}

	
	private List<Ingredient> filterByType (List<Ingredient> ingredients, Type type){
		//.stream().filter() 过滤List对象
		//x -> x.getType().equals(type) 满足这个条件的
		//Collectors.toList()用来结束Stream流
		return ingredients.stream().filter(x -> x.getType().equals(type)).collect(Collectors.toList());
	}
}

完成了上面的任务还需要使用SQL创建表,resources文件夹下创建.sql文件

create table if not exists Ingredient(
id varchar(4) not null,
name varchar(25) not null,
type varchar(10) not null
);

create table if not exists Taco(
id identity,
name varchar(50) not null,
createdAT timestamp not null
);

create table if not exists Taco_Ingredients(
taco bigint not null,
ingredient varchar(4) not null
);

alter table Taco_Ingredients add foreign key (taco) references Taco(id);
alter table Taco_Ingredients add foreign key (ingredient) references Ingredient(id);

create table if not exists Taco_Order(
id identity,
deliveryName varchar(50) not null,
deliveryStreet varchar(50) not null,
deliveryCity varchar(50) not null,
deliveryState varchar(2) not null,
deliveryZip varchar(10) not null,
ccNumver varchar(16) not null,
ccExpiration varchar(5) not null,
ccCVV varchar(3) not null,
placedAT timestamp not null
);

create table if not exists Taco_Order_Tacos(
tacoOrder bigint not null,
taco bigint not null
);

alter table Taco_Order_Tacos add foreign key (tacoOrder) references Taco_Order(id);
alter table Taco_Order_Tacos add foreign key (taco) references Taco(id);

JPA

详情见JPA博客

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
    		<groupId>javax.persistence</groupId>
    		<artifactId>persistence-api</artifactId>
   			<version>1.0</version>
		</dependency>

如果想要使用不同的JPA实现,至少需要将Hibernate依赖排除出去并将你所选择的JPA库包含进来。比如使用EclipseLink……



保护Spring


启用Spring Security

<!-- for security -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>
		<dependency>
			<groupId>org.thymeleaf.extras</groupId>
			<artifactId>thymeleaf-extras-springsecurity5</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-test</artifactId>
			<scope>test</scope>
		</dependency>

配置好之后,应用启动的时候,自动配置功能会探测到Spring Security出现在了类路径中,因此它会初始化一些安全配置。


配置Spring Security


编写Spring Security的基础配置类

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web
                      .configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web
                      .configuration.WebSecurityConfigurerAdapter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation
           .authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web
           .builders.HttpSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@SuppressWarnings("deprecation")
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
private UserDetailsService userDetailsService;

@Override
protected void configure(HttpSecurity http) throws Exception {
  http
    .authorizeRequests()
      .antMatchers("/design", "/orders")
      .hasRole("USER")
      .antMatchers("/", "/**").access("permitAll")
      
    .and()
      .formLogin()
        .loginPage("/login")
        
    .and()
      .logout()
        .logoutSuccessUrl("/")
   
    .and()
      .csrf()
        .ignoringAntMatchers("/h2-console/**")
 
    .and()  
      .headers()
        .frameOptions()
          .sameOrigin()
    ;
}

@Bean
public PasswordEncoder encoder() {
	return new BCryptPasswordEncoder();
}

@Override
protected void configure(AuthenticationManagerBuilder auth)
    throws Exception {

  auth
    .userDetailsService(userDetailsService)
    .passwordEncoder(encoder());
  }
  
}

配置用户存储

用户的信息可以存储在内存之中,假如只有数量有限的几个用户,并且这个用户几乎不会发生变化。

@Override
protected void configure(AuthenticationManagerBuilder auth)
    throws Exception {

  auth.inMemoryAuthentication()
  .withUser("Alice")
  .password("12345")
  .authorities("ROLE_USER")
  .and()
  .withUser("Alina")
  .password()
  ……
  }

除了在内存中存储还可以基于JDBC存储。

@Autowired
DataSource dataSource;

@Override
protected void configure(AuthenticationManagerBuilder auth)
    throws Exception {
  auth
  .jdbcAuthentication()
  .dataSource(dataSource);
  }

Spring Security内部有查找用户信息时所执行的SQL查询语句。
当然也可以自己设置:

.usersByUsernameQuery("select username, password, enabled from Users where username=?")
.authoritiesByUsernameQuery("Select username, authority from UserAuthorities where username=?")
.passwordEncoder(encoder())

最后的是给密码进行转码,密码明文存储比较危险。
encoder()是指定的密码转码器。


Spring Data repository

不过最好是使用Spring Data repository来存储用户。

对于用户User类,需要实现Spring Security的UserDetails接口。
通过实现UserDetails接口,能够提供更多信息给框架,比如用户都被授予了哪些权限以及用户的账号是否可用。

getAuthorities()方法应该返回用户被授予权限的一个集合,包括其他的isxxx()方法表明用户的账号是否可用或过期。


import java.util.Arrays;
import java.util.Collection;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import lombok.AccessLevel;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;

@Entity
@Data
@NoArgsConstructor(access=AccessLevel.PRIVATE,force=true)
@RequiredArgsConstructor
public class User implements UserDetails{
	private static final long serialVersionUID = 1L;
	@Id
	@GeneratedValue(strategy=GenerationType.AUTO)
	private Long id;
	private final String username;
	private final String password;
	private final String fullname;
	private final String street;
	private final String city;
	private final String state;
	private final String zip;
	private final String phoneNumber;
	
	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		return Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"));
	}
	
	@Override
	public boolean isAccountNonExpired() {
		return true;
	}
	@Override
	public boolean isAccountNonLocked() {
		return true;
	}
	@Override
	public boolean isCredentialsNonExpired() {
		return true;
	}
	@Override
	public boolean isEnabled() {
		return true;
	}
}

这里getAuthorities()方法只是简单的返回一个集合,这个集合表明所有的用户都被授予了ROLE_USER权限,所有方法返回True,用户都是活跃的。

再定义repository接口:

import org.springframework.data.repository.CrudRepository;
import com.example.demo.User;

public interface UserRepository extends CrudRepository<User,Long>{
	User findByUsername(String username);
}

Spring Data JPA会在运行时自动生成这个接口的实现。

Spring Security的UserDetailsService是一个用户详情服务的接口,这个接口的实现会得到一个用户的用户名,并且要么返回查找到的UserDetails对象,要么根据用户名无法得到结果的时候抛出异常。

用户详情服务:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import com.example.demo.User;
import com.example.demo.jdbcdata.UserRepository;

@Service  
public class UserRepositoryUserDetailsService implements UserDetailsService{
	private UserRepository userRepo;
	
	@Autowired
	public UserRepositoryUserDetailsService(UserRepository userRepo) {
		this.userRepo = userRepo;
	}
	
	@Override
	public UserDetails loadUserByUsername(String username)throws UsernameNotFoundException{
		User user = userRepo.findByUsername(username);
		if(user != null) {
			return user;
		}
		throw new UsernameNotFoundException("User '"+username+"'not found");
	}
}

再次回到configure方法

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
  http
    .authorizeRequests()
      .antMatchers("/design", "/orders")
      .hasRole("USER")
      .antMatchers("/", "/**").access("permitAll")
    .and()
      .formLogin()
        .loginPage("/login")   
    .and()
      .logout()
        .logoutSuccessUrl("/")
    .and()
      .csrf()
        .ignoringAntMatchers("/h2-console/**")
    .and()  
      .headers()
        .frameOptions()
          .sameOrigin()
    ;
}

@Bean
public PasswordEncoder encoder() {
	return new BCryptPasswordEncoder();
}

@Override
protected void configure(AuthenticationManagerBuilder auth)
    throws Exception {

  auth
    .userDetailsService(userDetailsService)
    .passwordEncoder(encoder());
  }
  
}

auth
.userDetailsService(userDetailsService)
调用这个方法,将自动装配到SecurityConfig中的UserDetailsService实例传递进去,后面加一个密码转码器,这样数据库的密码就会加密:

上面encoder方法带有@Bean注解,它将用来在Spring应用上下文中声明PasswordEncoder bean。对于encoder()的任何调用都会被拦截,并且会返回应用上下文中的bean实例。


用户注册Controller

import com.example.demo.jdbcdata.UserRepository;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/register")
public class RegistrationController {
  
  private UserRepository userRepo;
  private PasswordEncoder passwordEncoder;

  public RegistrationController(
      UserRepository userRepo, PasswordEncoder passwordEncoder) {
    this.userRepo = userRepo;
    this.passwordEncoder = passwordEncoder;
  }
  
  @GetMapping
  public String registerForm() {
    return "registration";
  }
  
  @PostMapping
  public String processRegistration(RegistrationForm form) {
    userRepo.save(form.toUser(passwordEncoder));
    return "redirect:/login";
  }
}

保护Web请求

保护请求

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter{
	@Autowired
	private UserDetailsService userDetailsService;
	
	@Bean
	public PasswordEncoder encoder() {
		return new BCryptPasswordEncoder();
	}
	
	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception{
		auth.userDetailsService(userDetailsService).passwordEncoder(encoder());
	}
	
	@Override
	protected void configure(HttpSecurity http)throws Exception{
		//对authorizeRequests()的调用会返回一个对象ExpressionInterceptUrlRegistry表达式截获Url注册表
		//基于它可以指定URL路径和这些路径的安全需求
		//这里指定了两条安全规则
		//具备“ROLE_USER”权限的用户才能访问"/design"和"/orders"
		//其他的请求允许所有用户访问
		
		//替换内置的登录页,自定义登录页的路径 and方法表示已经完成了授权相关的配置,并且要添加其他的HTTP配置,在开始新的配置区域时可以多次调用and()
		http.authorizeRequests()
		.antMatchers("/design","/orders")
		.hasRole("USER")
		.antMatchers("/","/**").permitAll()
		.and()  
		.formLogin()
		.loginPage("/login")
		.and()
		.logout()
		.logoutSuccessUrl("/")
		.and()
        .csrf()
          .ignoringAntMatchers("/h2-console/**")
        .and()  
        .headers()
          .frameOptions()
            .sameOrigin();
		
		//只允许具备ROLE_USER权限的用户在星期二创建新taco:
		/**
		http.authorizeRequests()
		.antMatchers("/design","/orders")
		.access("hasRole('ROLE_USER')&&"+
		"T(java.util.Calendar).getInstance().get("+
		"T(jave.util.Calendar).DAY_IF_WEEK)=="+
		"T(java.util.Calendar).TUESDAY")
		.antMatchers("/","/**").access("permitAll");
		**/
	}
}

需要确保只有认证过的用户才能发起对/design、/orders的请求。其他的请求允许所有用户访问。
对authorizeRequests()的调用会返回一个对象,基于这个对象可用指定URL路径和这些路径的安全需求。
这些规则的顺序很重要,声明在前面的安全规则比后面声明的规则有更高优先级。
hasRole()和permitAll()只是众多方法中的两个。
并且可用使用access()方法,通过SpEL表达式来声明更丰富的安全规则。

http
    .authorizeRequests()
      .antMatchers("/design", "/orders")
        .access("hasRole('ROLE_USER')")
      .antMatchers("/", "/**").access("permitAll")

formLogin()告诉Spring Security自定义登录页的路径是什么。
当Spring Security断定用户没有认证并且需要登录的时候,他就会将用户重定向到该路径。

.and()
      .formLogin()
        .loginPage("/login")
        .defaultSuccessUrl("/design")

这样设置可以让用户登录成功后跳转到 /design
甚至还可以强制用户登录成功后统一访问设计页面,如果在登录前在访问其他页面登录后也会被重定向到设计页面,如下:

.and()
      .formLogin()
        .loginPage("/login")
        .defaultSuccessUrl("/design",true)

退出也是类似的。

当然还需要一个控制器来处理对该路径的请求,因为登录页非常简单,只有一个视图没有其他内容,可以在WebConfig中将其声明为一个视图控制器:

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer{
	@Override
	//registry:登记
	/**用这个注册一个或多个视图控制器*/
	public void addViewControllers(ViewControllerRegistry registry) {
		//将"/"传递进去,视图控制器将会针对该路径执行GET请求
		//这个方法会返回ViewControllerRegistration对象
		//基于这个对象调用setViewName方法
		//用它指明当请求“/”时要转发到home视图上
		registry.addViewController("/").setViewName("home");
		registry.addViewController("/abc").setViewName("home");
		registry.addViewController("/login");
	}
}

防止跨站请求伪造

跨站请求伪造(Cross-Site Request Forgery, CSRF)是一种常见的安全攻击,它会让用户在一个恶意的Web页面上填写信息,然后自动将表单以攻击受害者的身份提交到另外一个应用上。
为了防止这种类型的攻击,应用可以在展现表单的时候生成一个CSRF tocken,并放到隐藏域中,然后将其临时存储起来,以便后续在服务器上使用。在提交表单的时候,tocken将和其他的表单数据一起发送至服务器端。请求会被服务器拦截,并与最初生成的token进行对比,如果token匹配,那么请求将会允许处理。

Spring Security提供了内置的CSRF保护,默认就是启用的,不需要显示配置它,唯一做的就是确保每个表单有一个_csrf字段,它会持有CSRF token。
Spring Security甚至简化了将token放到请求的_csrf属性,在Thymeleaf模板中,可以这样在隐藏域中渲染CSRF token:

<input type="hidden" name="_csrf" th:value="${_csrf.token}"/>

如果使用Spring MVC的JSP标签库或者Spring Security的Thymeleaf方言,甚至都不用这样明确包含这个隐藏域。
在Thymeleaf中,只要确保< form >的某个属性带有Thymeleaf属性前缀即可。
比如:

<form method="POST" th:action="@{/login}" id="loginForm">

用户是谁

在Order实体和User实体之间实现所需的关联:
@ManyToOne

@Data
@Entity
@Table(name="Taco_Order")
public class Order implements Serializable {

  private static final long serialVersionUID = 1L;
  
  @Id
  @GeneratedValue(strategy=GenerationType.AUTO)
  private Long id;
  
  @ManyToOne  //一个订单只能属于一个用户,但是一个用户却可以有多个订单
  private User user;
  ……

在OrderController中,修改processOrder方法:
已认证用户的信息可以借助@AuthenticationPrincipal注解将其注入到控制器中。

@PostMapping
  public String processOrder(@Valid Order order, Errors errors, SessionStatus sessionStatus,
		  @AuthenticationPrincipal User user) {
    if (errors.hasErrors()) {
      return "orderForm";
   }
   
    order.setUser(user);
    
    orderRepo.save(order);
    sessionStatus.setComplete();
    
    return "redirect:/";
  }

创建REST服务

这里使用Spring来提供REST API。
需要用到的有Spring MVC,使用Spring MVC的控制器创建RESTful端点。
还要将Spring Data repository暴露为REST端点。


端点

这个端点会处理针对“/design/recent”的HTTP GET请求并将最近设计的taco列表作为响应。

@RestController  //REST控制器
@RequestMapping(path="/design",produces="application/json")
@CrossOrigin(origins="*")  //允许跨域请求
public class DesignTacoController {
	private TacoRepository tacoRepo;
	@Autowired
	EntityLinks entityLinks;
	public DesignTacoController(TacoRepository tacoRepo) {
		this.tacoRepo = tacoRepo;
	}
	
	@GetMapping("/recent")
	public Iterable<Taco> recentTacos(){
		//获取最近设计的Taco
		PageRequest page = PageRequest.of(0, 12, Sort.by("createdAt").descending());
		return tacoRepo.findAll(page).getContent();
	}
	
	@GetMapping("/{id}")
	  public Taco tacoById(@PathVariable("id") Long id) {
	    Optional<Taco> optTaco = tacoRepo.findById(id);
	    if (optTaco.isPresent()) {
	      return optTaco.get();
	    }
	    return null;
	  }
}

这是一个REST控制器,由@RestController注解声明的。

这个注解与REST最密切相关的点在于控制器中所有处理器方法的返回值都要写入响应体中,而不是将值放到模型中并传递给一个视图以便于进行渲染。

作为替代方案也可以用@Controller注解,不过这样就要为每个处理器方法再加上@ResponseBody注解。另外一种方案就是返回ResponseEntity对象。

@RequestMapping(path="/design",produces="application/json")

这个注解的produces属性指明DesignTacoController中的所有处理器方法只会处理Accept头信息包含"application/json"的请求。
不仅会限制API只会生成JSON结果,同时还允许其他的控制器处理具有相同路径的请求,只要这些请求不要求JSON格式的输出就可以。
这样就会允许我们设置多个内容类型,比如生成XML格式的:

@RequestMapping(path="/design",produces={"application/json","test/xml"})
@CrossOrigin(origins="*")

因为客户端部分将会运行在与API相独立的主机和/或端口上,Web浏览器会阻止客户端消费该API。我们可以在服务端响应中添加CORS(跨域资源共享 Cross-Origin Resource Sharing)头信息来突破这一限制。

Spring借助@CrossOrigin注解让CORS的使用更加简单。
@CrossOrigin允许来自任何域的客户端消费该API。

recentTacos()方法中的逻辑非常简单直接,它构建了一个PageRequest对象,指明我们想要第一页(序号为0)的12条结果,并且要按照taco的创建时间降序排列。
PageRequest会被传递到TacoRepository的findAll()方法中,分页的结果内容则会返回到客户端。

假如要提供一个按照ID抓取单个taco的端点,通过在处理器方法的路径上使用占位符并让方法接收一个路径变量,捕获到这个ID就能查找。

@GetMapping("/{id}")
  public Taco tacoById(@PathVariable("id") Long id) {
    Optional<Taco> optTaco = tacoRepo.findById(id);
    if (optTaco.isPresent()) {
      return optTaco.get();
    }
    return null;
  }

这个控制器方法处理的是针对"/design/{id}"的GET请求,其中路径的{id}部分是占位符,请求中的实际参数会传递给id参数,会通过@PathVariable注解与{id}占位符进行匹配。
id参数传递到了repository的findById()方法中,以便于抓取Taco,findById()返回的是一个Optional<Taco>,如果能够匹配,返回实际的Taco。
(Optional 类主要解决的问题是臭名昭著的空指针异常)

如果ID无法匹配任何已知的Taco,会返回null。返回null客户端将会接收到空的响应体以及值为200(OK)的HTTP状态码。客户端实际上接收到了一个无法使用的响应,但是状态码却提示一切正常,更好的方法是在响应中使用HTTP 404(NOT FOUND)状态。

@GetMapping("/{id}")
	public ResponseEntity<Taco> tacoById(@PathVariable("id") Long id) {
		Optional<Taco> optTaco = tacoRepo.findById(id);
		if (optTaco.isPresent()) {
			return new ResponseEntity<>(optTaco.get(), HttpStatus.OK);
		}
		return new ResponseEntity<>(null, HttpStatus.NOT_FOUND);
	}

这样tacoById()返回的不是一个Taco对象,而是ResponseEntity< Taco>。
如果找到,将Taco包装到ResponseEntity中,并且会带有OK的HTTP状态。
如果找不到,包装一个null,并且会带有NOT FOUND的HTTP状态,从而表明客户端视图抓取的taco并不存在。

除了面向客户端的初始Taco Cloud API,当然它也可以用于其他类型的客户端。
还可以用curl或HTTPie这样的命令行工具来探测该API。

(curl 是常用的命令行工具,用来请求 Web 服务器。它的名字就是客户端(client)的 URL 工具的意思。
HTTPie 是一个 HTTP 的命令行客户端,目标是让 CLI 和 web 服务之间的交互尽可能的人性化。这个工具提供了简洁的 http 命令,允许通过自然的语法发送任意 HTTP 请求数据,展示色彩化的输出。HTTPie 可用于与 HTTP 服务器做测试、调试和常规交互。)

$ curl localhost:8080/design/recent
$ http :8080/design/recent

发送数据到服务器端

客户端通过POST方法向API发送数据。
将一个taco设计存放到变量中借助HTTP POST请求发送至"/design"的API端点上。

需要在DesignTacoController中编写一个方法处理该请求并保存该taco设计

@PostMapping(consumes="application/json")
	@ResponseStatus(HttpStatus.CREATED)
	public Taco postTaco(@RequestBody Taco taco) {
		return tacoRepo.save(taco);
	}

这里设置了consumes属性,consumes属性用于指定请求输入,而produces用于指定请求输出。

Taco参数带有@RequestBody注解,表明请求应该被转化为一个Taco对象并绑定到该参数上。这个注解非常重要,如果没有Spring MVC会认为我们希望将请求参数绑定到Taco上,但是@RequestBody注解能够确保请求体中的JSON会被绑定到Taco对象上

在postTaco()接收到了Taco对象只会,就会将该对象传递给TacoRepository的save()方法。

我们为postTaco方法添加了@ResponseStatus(HTTPStatus.CREATED)注解。在正常的情况下(没有异常抛出的时候),所有HTTP的状态码都是200 OK,表明请求是成功的。但是我们这里希望在POST请求的情况下,获得201(CREATE)的HTTP状态更具有描述性,这会告诉客户端,请求不仅成功了,还创建了一个资源,在适当的地方使用ResponseStatus将最具描述性和最精确的HTTP状态码传递给客户端是一种更好的做法。


在服务器上更新数据

尽管PUT经常被用来更新资源,但是它在语义上其实是GET的对立面,GET请求用来从服务端往客户端传输数据,而PUT请求则是从客户端往服务器端发送数据

PUT真正的目的是执行大规模的替换操作,而不是更新操作。
HTTP PATCH的目的是对资源数据打补丁或局部更新。

假设想要更新某个订单的地址信息,借助REST API,其中有一种实现方式就是借助PUT请求处理器

@PutMapping("/{orderId}")
public Order putOrder(@RequestBody Order order){
	return repo.save(oredr);
}

这种方式可以运行,但是可能需要客户端将完整的订单数据从PUT请求中提交上来。从语义上讲,PUT意味着“将这个数据放到这个URL上”,本质上就是替换已有的数据。如果省略了订单上的某个属性,那么该属性的值应该被null所覆盖。

所以PUT请求所作的是对资源数据的大规模替换,对局部更新的请求要使用HTTP PATCH请求和Spring的@PatchMapping注解。

@PatchMapping(path="/{orderId}",consumes="application/json")
//@PathVariable("orderId")占位符匹配
	 public Order patchOrder(@PathVariable("orderId") Long orderId,
			                 @RequestBody Order patch) {
		 Order order = repo.findById(orderId).get();
		 if(patch.getDeliveryName()!=null) {
			 order.setDeliveryName(patch.getDeliveryName());
		 }
	     if (patch.getDeliveryStreet() != null) {
	      order.setDeliveryStreet(patch.getDeliveryStreet());
	    }
	    if (patch.getDeliveryCity() != null) {
	      order.setDeliveryCity(patch.getDeliveryCity());
	    }
	    if (patch.getDeliveryState() != null) {
	      order.setDeliveryState(patch.getDeliveryState());
	    }
	    if (patch.getDeliveryZip() != null) {
	      order.setDeliveryZip(patch.getDeliveryState());
	    }
	    if (patch.getCcNumber() != null) {
	      order.setCcNumber(patch.getCcNumber());
	    }
	    if (patch.getCcExpiration() != null) {
	      order.setCcExpiration(patch.getCcExpiration());
	    }
	    if (patch.getCcCVV() != null) {
	      order.setCcCVV(patch.getCcCVV());
	    }
	    return repo.save(order);
	 }

删除服务器上的数据

客户端通过HTTP DELETE请求的形式要求移除某个资源。

Spring MVC的@DeleteMapping注解能够便利地声明处理DELETE请求的方法。

    @DeleteMapping("/{orderId}")
	@ResponseStatus(code=HttpStatus.NO_CONTENT)
	public void deleteOrder(@PathVariable("orderId") Long orderId) {
		try {
			repo.deleteById(orderId);
		}catch(EmptyResultDataAccessException e) {}
	}

启用超媒体

超媒体作为应用状态引擎(Hypermedia as the Engin of Application State, HATEOAS)是一种创建自描述API的方式。API所返回的资源中会包含相关资源的链接,客户端只需要了解最少的API URL信息就能导航整个API。
这种方式能够掌握API所提供的资源之间的关系,客户端能够基于API的URL中所发现的关系对它们进行遍历。

没有超链接的情况下,客户端以JSON格式接收到的taco列表会如下所示:

{
 "id":4,
 "name":"Veg-Out",
 "createdAt":"2020-01-……"
 "ingredients:"{
  {"id":"FLTO","name":"Flour Tortilla","type":"WRAP"},
  {"id":"COTO","name":"Corn Tortilla","type":"WRAP"},
  ……
},
……

如果API启用了超媒体功能,那么API将会描述自己的URL,从而减轻客户端对其进行硬编码的痛苦。如果嵌入超链接会如下所示:

{
"_embedded":{
 "tacoResourceList":[
 {
   "name":"Veg-Out",
   "CreatedAt":"……",
   "ingredients":{
     "name":"Flour Tortilla","type":"WRAP",
     "_links":{
       "self":{ "href":"http://localhost:8080/ingredients/FLTO"
       }
     },
     ……
   }
 }
 ]
},
"_links":{
  "recents":{
     "href":"http://localhost:8080/design/recent"
  }
}
}

这种特殊风格的HATEOAS被称为HAL(超文本应用语言,Hypertext Application Language)。这是一种在JSON响应中嵌入超链接的简单通用格式。

列表中的每个元素都包含了一个_links的属性,为客户端提供导航API的超链接。在本例中,taco和配料都有一个self链接,用来引用该资源,整个列表有一个recents链接,用来引用该API自身。

如果客户端应用需要多列表中的taco执行HTTP请求,那么在开发的时候不需要关心资源的URL是什么样子,相反,它只需要请求"self"链接就可以了,该属性将会映射到http://localhost:8080/design/4。
如果客户端想要处理特定的配料,只需要查找该配料的self链接即可。

Spring HATEOAS项目为Spring提供了超链接的支持,它提供了一些类和资源装配器(assembler),在Spring MVC控制器返回资源之前能够为其添加链接。

启用超媒体,需要在构建文件添加Spring HATEOAS starter依赖。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值